diff --git a/apps/api/migrations/20261030193055_add_moderation_warnings/migration.sql b/apps/api/migrations/20261030193055_add_moderation_warnings/migration.sql new file mode 100644 index 000000000..cf29b4617 --- /dev/null +++ b/apps/api/migrations/20261030193055_add_moderation_warnings/migration.sql @@ -0,0 +1,90 @@ +CREATE TABLE "core_moderation_sanctions" ( + "id" serial PRIMARY KEY, + "userId" integer NOT NULL, + "type" varchar(32) NOT NULL, + "source" varchar(32) NOT NULL, + "startsAt" timestamp DEFAULT now() NOT NULL, + "endsAt" timestamp, + "warningId" integer UNIQUE, + "thresholdId" integer, + "thresholdSnapshot" jsonb, + "createdById" integer, + "createdAt" timestamp DEFAULT now() NOT NULL, + "revokedAt" timestamp, + "revokedById" integer, + "revokeReason" text +); +--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warning_reasons" ( + "id" serial PRIMARY KEY, + "points" integer DEFAULT 0 NOT NULL, + "expiresAfterHours" integer, + "enabled" boolean DEFAULT true NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warning_reasons" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warning_thresholds" ( + "id" serial PRIMARY KEY, + "points" integer NOT NULL, + "action" varchar(32) NOT NULL, + "durationHours" integer, + "enabled" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warning_thresholds" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_moderation_warnings" ( + "id" serial PRIMARY KEY, + "userId" integer NOT NULL, + "issuedById" integer, + "reasonId" integer, + "reasonSnapshot" jsonb, + "points" integer NOT NULL, + "defaultPoints" integer, + "overrideNote" text, + "message" text NOT NULL, + "staffNote" text, + "issuedAt" timestamp DEFAULT now() NOT NULL, + "expiresAt" timestamp, + "pointsBefore" integer DEFAULT 0 NOT NULL, + "pointsAfter" integer DEFAULT 0 NOT NULL, + "pluginId" varchar(100), + "targetType" varchar(100), + "targetId" varchar(64), + "scopeFamily" varchar(100), + "scopeId" varchar(64), + "contentSnapshot" jsonb, + "reportId" integer, + "idempotencyKey" varchar(64), + "revokedAt" timestamp, + "revokedById" integer, + "revokeReason" text, + CONSTRAINT "core_moderation_warnings_idempotency_key" UNIQUE("issuedById","idempotencyKey") +); +--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE INDEX "core_moderation_sanctions_user_idx" ON "core_moderation_sanctions" ("userId","revokedAt","endsAt");--> statement-breakpoint +CREATE INDEX "core_moderation_sanctions_created_at_id_idx" ON "core_moderation_sanctions" ("createdAt","id");--> statement-breakpoint +CREATE INDEX "core_moderation_warning_reasons_position_idx" ON "core_moderation_warning_reasons" ("position");--> statement-breakpoint +CREATE UNIQUE INDEX "core_moderation_warning_thresholds_enabled_points_idx" ON "core_moderation_warning_thresholds" ("points") WHERE "enabled" = true;--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_user_idx" ON "core_moderation_warnings" ("userId","issuedAt");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_issued_at_id_idx" ON "core_moderation_warnings" ("issuedAt","id");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_issued_by_idx" ON "core_moderation_warnings" ("issuedById");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_reason_idx" ON "core_moderation_warnings" ("reasonId");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_target_idx" ON "core_moderation_warnings" ("pluginId","targetType","targetId");--> statement-breakpoint +CREATE INDEX "core_moderation_warnings_report_idx" ON "core_moderation_warnings" ("reportId");--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_rlFetAHphSkc_fkey" FOREIGN KEY ("warningId") REFERENCES "core_moderation_warnings"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_ejX3lH3GN7oz_fkey" FOREIGN KEY ("thresholdId") REFERENCES "core_moderation_warning_thresholds"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_createdById_core_users_id_fkey" FOREIGN KEY ("createdById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_sanctions" ADD CONSTRAINT "core_moderation_sanctions_revokedById_core_users_id_fkey" FOREIGN KEY ("revokedById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_issuedById_core_users_id_fkey" FOREIGN KEY ("issuedById") REFERENCES "core_users"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_J1JvecLI20Z2_fkey" FOREIGN KEY ("reasonId") REFERENCES "core_moderation_warning_reasons"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_0vlCswFpT46n_fkey" FOREIGN KEY ("reportId") REFERENCES "core_moderation_reports"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_moderation_warnings" ADD CONSTRAINT "core_moderation_warnings_revokedById_core_users_id_fkey" FOREIGN KEY ("revokedById") REFERENCES "core_users"("id") ON DELETE SET NULL; \ No newline at end of file diff --git a/apps/api/migrations/20261030193055_add_moderation_warnings/snapshot.json b/apps/api/migrations/20261030193055_add_moderation_warnings/snapshot.json new file mode 100644 index 000000000..226fce40f --- /dev/null +++ b/apps/api/migrations/20261030193055_add_moderation_warnings/snapshot.json @@ -0,0 +1,24672 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "d0a2bdea-582a-4e74-831e-21a73f67bd7b", + "prevIds": [ + "ac6bc2f3-4afc-4768-be17-a5b36fd6180e" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "core_admin_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_action_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_budget_periods", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_calls", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_cost_adjustments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_reservations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_policies", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_translation_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_user_overrides", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_audit_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_audit_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_containers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_documents", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_drafts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_field_locks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_submissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_file_refs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_revisions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_schedules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_slug_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_cron", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_dashboard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_analysis", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages_words", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_report_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_report_reasons", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_reports", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderators_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_deliveries", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_receipts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_user_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notifications", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_page_layouts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkey_challenges", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_payments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_payments_webhook_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_queue", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_index", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_popularity", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_secrets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions_known_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_lock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_results", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sso_attempts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sso_pending", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_confirm_emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_forgot_password", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_secondary_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_operations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_profile_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_sanctions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warning_reasons", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warning_thresholds", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_warnings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_author_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_category_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_topics", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_bulletins", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_bulletins_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_related_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_events_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_faq", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_related_explainers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_explainers_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_letters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_stories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_stories_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags_related_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "newsroom_tags_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "payments_demo_purchases", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fallbackModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxInputCharacters", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeoutMs", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxSteps", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instructions", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "unit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "limitAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "spentAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unknownCount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerRequestId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costReason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reconciledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deltaUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "budgetPeriodId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "granted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "promptVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "settlement", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "leaseExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "acceptedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "systemMonthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "defaultMonthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "userRequestsPerMinute", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "userConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "systemConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "365", + "generated": null, + "identity": null, + "name": "historyRetentionDays", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "altEnabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "altBatchSize", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "altScanCursor", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "altScanStartedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceLocale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "blocked", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "area", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(150)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorName", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetLabel", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "changes", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "correlationId", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_audit_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "365", + "generated": null, + "identity": null, + "name": "retentionDays", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_audit_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "policies", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(35)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "acquiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerDefinitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseValues", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseUpdatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reviewerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "decidedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "changedFields", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "restoredFromRevisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledFor", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effectsError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedule", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "widgets", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folder", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'automatic'", + "generated": null, + "identity": null, + "name": "altPolicy", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "time24", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tableName", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "variable", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'GET'", + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'localhost'", + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "500", + "generated": null, + "identity": null, + "name": "statusCode", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reportId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "requiresDetails", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reporterId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetTitle", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'new'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assigneeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolution", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolutionNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openKey", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "itemCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerMessageId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "skipReason", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sentAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "schemaVersion", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "recipientIds", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowSelf", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "recipientCursor", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notificationId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailPending", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emailDeliveryId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unreadCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "preferences", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "groupBucket", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "latestEventId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "eventCount", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "activitySeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "readSeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archivedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastActivityAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "zones", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ceremony", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(1024)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceType", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "backedUp", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reference", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerType", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerStatus", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checkoutId", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "checkoutUrl", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "livemode", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "syncedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "paymentId", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "receivedAt", + "entityType": "columns", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'default'", + "generated": null, + "identity": null, + "name": "queue", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reservedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dedupeKey", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "root", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "guest", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowUploadFiles", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalMaxStorage", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxStorageForSubmit", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadAvatar", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2048", + "generated": null, + "identity": null, + "name": "maxAvatarSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadCover", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5120", + "generated": null, + "identity": null, + "name": "maxCoverSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditPersonalInfo", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowBrowseAnonymously", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "forceBrowseAnonymously", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "editOwnContentMinutes", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowDeleteOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowHideOwnContent", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "authorIds", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"title\", '')), 'A') || setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"content\", '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_vector", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isPublic", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isHidden", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "imageId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "indexedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logScore", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clicks", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "comments", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "owner", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastManualAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(214)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "componentId", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "versions", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasUnknownVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(256)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coreVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "etag", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastModified", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stateHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bindingHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "codeVerifier", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonce", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "profile", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmHash", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "confirmSends", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "confirmSentAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sso_pending" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nameCode", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "headline", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showRealName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(6)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarColor", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "birthday", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeZone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSeenAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hideOnlineStatus", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerEmail", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syncOnSignIn", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSha256", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarFileId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "intent", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "fields", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "codeVerifier", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nonce", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "startsAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endsAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "warningId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thresholdId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thresholdSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokeReason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAfterHours", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "durationHours", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasonId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasonSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "defaultPoints", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "overrideNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "staffNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "issuedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pointsBefore", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pointsAfter", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reportId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revokeReason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "blog_comments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImage", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImageAlt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topic", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "forum", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_articles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "varchar(80)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deletedBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "newsroom_comments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "byDefault", + "name": "newsroom_events_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "primaryTag", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startsAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "syndicationIndexable", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syndicationNoIndex", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "syndicationPriority", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "question", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoTitle", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoDescription", + "entityType": "columns", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "desk", + "entityType": "columns", + "schema": "public", + "table": "newsroom_letters" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "newsroom_pages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "noIndex", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animation", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenReason", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "periodStart", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_budget_periods_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_run_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "costSource", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reconciledAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_reconcile_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "callId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_cost_adjustments_call_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "budgetPeriodId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_run_period_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actionKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_action_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "leaseExpiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"idempotencyKey\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_translation_sources_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_actor_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_plugin_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_action_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "area", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_area_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeFamily", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_audit_logs_scope_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_parent_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_doc_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_pending_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_queue_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "revisionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_file_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_item_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_translation_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_actor_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scheduledFor", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_shared_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_locale_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastRun", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_cron_last_run_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_cron" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_dashboard_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_file_language_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileFingerprint", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_analysis_file_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status <> 'completed'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_state_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lang_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tableName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "variable", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reportId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_report_events_report_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_report_reasons_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_report_reasons" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "assigneeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_assignee_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reporterId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_reporter_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeFamily", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_scope_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_claim_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "eventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"eventId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "notificationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "seq", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_notification_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"emailPending\" = true AND \"emailDeliveryId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_email_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "unreadCount", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_user_state_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupBucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_group_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_inbox_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL AND \"readSeq\" < \"activitySeq\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subjectType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "subjectId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_subject_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastActivityAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_last_activity_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "latestEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_latest_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webauthnUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_webauthn_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reference", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_open_reference_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "checkoutId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"checkoutId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_checkout_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reference", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_reference_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_open_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "receivedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_payments_webhook_events_received_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_status_available_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "completedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"completedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_completed_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "dedupeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"dedupeKey\" IS NOT NULL AND status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_dedupe_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_roles_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "search_vector", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_search_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "to_tsvector('simple'::regconfig, \"title\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_title_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorIds", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_author_ids_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_item_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_language_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isPublic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_is_public_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "imageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_image_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "containerType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_container_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "logScore", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_popularity_log_score_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_popularity" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ipAddress", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_known_devices_ip_address_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sso_attempts_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sso_attempts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sso_pending_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sso_pending" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_avatar_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_cover_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "revokedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "endsAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_sanctions_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_sanctions_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warning_reasons_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warning_reasons" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "points", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"enabled\" = true", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warning_thresholds_enabled_points_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warning_thresholds" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_issued_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuedById", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_issued_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reasonId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_reason_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reportId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_warnings_report_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_article_parent_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_article_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverImage", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_cover_image_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_parent_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "forum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_forum_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_bulletins_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "article", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_article_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deletedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_comments_deleted_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "primaryTag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_primary_tag_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_related_events_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_related_events_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_sessions_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_events_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "syndicationPriority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_syndication_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_categories_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_categories_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_faq_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_related_explainers_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_related_explainers_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_explainers_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "desk", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_desk_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_letters_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_pages_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_code_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "animation", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_animation_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_stories_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_related_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_related_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "newsroom_tags_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "payments_demo_purchases_user_item_unique", + "entityType": "indexes", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_calls_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": false, + "columns": [ + "callId" + ], + "schemaTo": "public", + "tableTo": "core_ai_calls", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_callId_core_ai_calls_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "budgetPeriodId" + ], + "schemaTo": "public", + "tableTo": "core_ai_budget_periods", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_FClYpPXZwvsm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_permissions_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_policies_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_runs_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_translation_sources_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_user_overrides_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_audit_logs_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_audit_logs" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_content_containers_q6QIucJ4HJ7B_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_documents_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_drafts_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_field_locks_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_authorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "reviewerId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_reviewerId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "revisionId" + ], + "schemaTo": "public", + "tableTo": "core_content_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_file_refs_5get6SfhBPHr_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "core_content_file_refs_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_revisions_actorUserId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_schedules_createdBy_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_dashboard_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_languageCode_core_languages_code_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_analysis_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_analysis_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_state_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_languages_words_languageCode_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_logs_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": false, + "columns": [ + "reportId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_reports", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_report_events_4CqHOxfheaUm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_report_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "reporterId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_reporterId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "assigneeId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_assigneeId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "resolvedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_resolvedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_deliveries_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_deliveries_vcf37BytIa0M_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_UQ7ZEs0I6ijx_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "notificationId" + ], + "schemaTo": "public", + "tableTo": "core_notifications", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_Vxye2Cs6MgcL_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_user_state_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notifications_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "latestEventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_notifications_I8QoGbTal5Wt_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkey_challenges_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkeys_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_payments_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_payments" + }, + { + "nameExplicit": false, + "columns": [ + "paymentId" + ], + "schemaTo": "public", + "tableTo": "core_payments", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_payments_webhook_events_paymentId_core_payments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "nameExplicit": false, + "columns": [ + "imageId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_search_index_imageId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "core_users_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "language" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET DEFAULT", + "name": "core_users_language_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "avatarId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_avatarId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "coverId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_coverId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_forgot_password_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "avatarFileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_sso_avatarFileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_operations_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "schemaTo": "public", + "tableTo": "core_users_sso", + "columnsTo": [ + "userId", + "providerId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_connection_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_sanctions_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "warningId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warnings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_rlFetAHphSkc_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "thresholdId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warning_thresholds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_ejX3lH3GN7oz_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "createdById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_createdById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "revokedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_sanctions_revokedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_sanctions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_warnings_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "issuedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_issuedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "reasonId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_warning_reasons", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_J1JvecLI20Z2_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "reportId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_reports", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_0vlCswFpT46n_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "revokedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_warnings_revokedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_categories_translations_itemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_categories_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "article" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_comments_article_blog_posts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "blog_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_comments_parent_blog_comments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_posts_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "coverImage" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_coverImage_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_relatedItemId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_category_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_category_id_relatedItemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_translations_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "topic" + ], + "schemaTo": "public", + "tableTo": "forum_topics", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "forum_replies_topic_forum_topics_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "forum_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_parent_forum_replies_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "forum" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "forum_topics_forum_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_articles_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_articles_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_bulletins", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_bulletins_translations_kgMZwrpzwwU7_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_bulletins_translations_rkkiMJQiOrEo_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "deletedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_deletedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "article" + ], + "schemaTo": "public", + "tableTo": "newsroom_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_comments_article_newsroom_articles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "newsroom_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_comments_parent_newsroom_comments_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "primaryTag" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_primaryTag_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_events_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_gallery_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_related_events_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_related_events_0xHNSLM3JAJj_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_sessions_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_tags_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_events_translations_itemId_newsroom_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_events_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_categories_9dOmLv3k6xDX_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_categories_mnxKvproEUm5_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_faq_itemId_newsroom_explainers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_faq" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_related_explainers_TFvIU3TogSGy_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_related_explainers_74JhaXF5PsDV_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_tags_itemId_newsroom_explainers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_explainers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_explainers_translations_hcPB0jdTm8MV_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_explainers_translations_qqggtF6jhcsZ_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_letters_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_letters_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "desk" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_letters_desk_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_letters" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_stories_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "animation" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_animation_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_stories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_stories_gallery_itemId_newsroom_stories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_stories_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_tags_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "newsroom_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "newsroom_tags_category_newsroom_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_related_tags_itemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_related_tags_relatedItemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "newsroom_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "newsroom_tags_translations_itemId_newsroom_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "newsroom_tags_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "payments_demo_purchases_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "payments_demo_purchases" + }, + { + "columns": [ + "roleId", + "permission" + ], + "nameExplicit": false, + "name": "core_ai_role_permissions_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "columns": [ + "eventId", + "userId" + ], + "nameExplicit": false, + "name": "core_notification_receipts_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "columns": [ + "providerId", + "eventId" + ], + "nameExplicit": false, + "name": "core_payments_webhook_events_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_payments_webhook_events" + }, + { + "columns": [ + "itemType", + "itemId" + ], + "nameExplicit": false, + "name": "core_search_popularity_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_search_popularity" + }, + { + "columns": [ + "userId", + "roleId" + ], + "nameExplicit": false, + "name": "core_users_secondary_roles_userId_roleId_pk", + "entityType": "pks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "columns": [ + "userId", + "field" + ], + "nameExplicit": false, + "name": "core_users_sso_profile_sources_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_categories_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_author_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_category_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_posts_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_bulletins_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_bulletins_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_related_events_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_related_events" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_events_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_events_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_events_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_categories_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_categories" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_related_explainers_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_related_explainers" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_explainers_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_explainers_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_stories_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_stories_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "newsroom_tags_related_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_tags_related_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "newsroom_tags_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "newsroom_tags_translations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_permissions_pkey", + "schema": "public", + "table": "core_admin_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_sessions_pkey", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "pks" + }, + { + "columns": [ + "actionKey" + ], + "nameExplicit": false, + "name": "core_ai_action_settings_pkey", + "schema": "public", + "table": "core_ai_action_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_budget_periods_pkey", + "schema": "public", + "table": "core_ai_budget_periods", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_calls_pkey", + "schema": "public", + "table": "core_ai_calls", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_cost_adjustments_pkey", + "schema": "public", + "table": "core_ai_cost_adjustments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_reservations_pkey", + "schema": "public", + "table": "core_ai_reservations", + "entityType": "pks" + }, + { + "columns": [ + "roleId" + ], + "nameExplicit": false, + "name": "core_ai_role_policies_pkey", + "schema": "public", + "table": "core_ai_role_policies", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_runs_pkey", + "schema": "public", + "table": "core_ai_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_settings_pkey", + "schema": "public", + "table": "core_ai_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_translation_sources_pkey", + "schema": "public", + "table": "core_ai_translation_sources", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_ai_user_overrides_pkey", + "schema": "public", + "table": "core_ai_user_overrides", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_audit_logs_pkey", + "schema": "public", + "table": "core_audit_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_audit_settings_pkey", + "schema": "public", + "table": "core_audit_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_containers_pkey", + "schema": "public", + "table": "core_content_containers", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_documents_pkey", + "schema": "public", + "table": "core_content_documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_drafts_pkey", + "schema": "public", + "table": "core_content_drafts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_field_locks_pkey", + "schema": "public", + "table": "core_content_field_locks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_submissions_pkey", + "schema": "public", + "table": "core_content_submissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_file_refs_pkey", + "schema": "public", + "table": "core_content_file_refs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_revisions_pkey", + "schema": "public", + "table": "core_content_revisions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_schedules_pkey", + "schema": "public", + "table": "core_content_schedules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_slug_history_pkey", + "schema": "public", + "table": "core_content_slug_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_cron_pkey", + "schema": "public", + "table": "core_cron", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_dashboard_pkey", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_pkey", + "schema": "public", + "table": "core_files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_pkey", + "schema": "public", + "table": "core_files_alt", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_analysis_pkey", + "schema": "public", + "table": "core_files_alt_analysis", + "entityType": "pks" + }, + { + "columns": [ + "fileId" + ], + "nameExplicit": false, + "name": "core_files_alt_state_pkey", + "schema": "public", + "table": "core_files_alt_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_pkey", + "schema": "public", + "table": "core_languages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_words_pkey", + "schema": "public", + "table": "core_languages_words", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_logs_pkey", + "schema": "public", + "table": "core_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_report_events_pkey", + "schema": "public", + "table": "core_moderation_report_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_report_reasons_pkey", + "schema": "public", + "table": "core_moderation_report_reasons", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_reports_pkey", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderators_permissions_pkey", + "schema": "public", + "table": "core_moderators_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_deliveries_pkey", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_events_pkey", + "schema": "public", + "table": "core_notification_events", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "core_notification_settings_pkey", + "schema": "public", + "table": "core_notification_settings", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_notification_user_state_pkey", + "schema": "public", + "table": "core_notification_user_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notifications_pkey", + "schema": "public", + "table": "core_notifications", + "entityType": "pks" + }, + { + "columns": [ + "pageId" + ], + "nameExplicit": false, + "name": "core_page_layouts_pkey", + "schema": "public", + "table": "core_page_layouts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkey_challenges_pkey", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkeys_pkey", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_payments_pkey", + "schema": "public", + "table": "core_payments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_queue_pkey", + "schema": "public", + "table": "core_queue", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_roles_pkey", + "schema": "public", + "table": "core_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_search_index_pkey", + "schema": "public", + "table": "core_search_index", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_secrets_pkey", + "schema": "public", + "table": "core_secrets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_pkey", + "schema": "public", + "table": "core_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_known_devices_pkey", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_update_lock_pkey", + "schema": "public", + "table": "core_update_lock", + "entityType": "pks" + }, + { + "columns": [ + "componentId" + ], + "nameExplicit": false, + "name": "core_update_results_pkey", + "schema": "public", + "table": "core_update_results", + "entityType": "pks" + }, + { + "columns": [ + "url" + ], + "nameExplicit": false, + "name": "core_update_sources_pkey", + "schema": "public", + "table": "core_update_sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sso_attempts_pkey", + "schema": "public", + "table": "core_sso_attempts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sso_pending_pkey", + "schema": "public", + "table": "core_sso_pending", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_pkey", + "schema": "public", + "table": "core_users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_confirm_emails_pkey", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_forgot_password_pkey", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_sso_operations_pkey", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_sanctions_pkey", + "schema": "public", + "table": "core_moderation_sanctions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warning_reasons_pkey", + "schema": "public", + "table": "core_moderation_warning_reasons", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warning_thresholds_pkey", + "schema": "public", + "table": "core_moderation_warning_thresholds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_warnings_pkey", + "schema": "public", + "table": "core_moderation_warnings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_categories_pkey", + "schema": "public", + "table": "blog_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_comments_pkey", + "schema": "public", + "table": "blog_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_posts_pkey", + "schema": "public", + "table": "blog_posts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_replies_pkey", + "schema": "public", + "table": "forum_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_topics_pkey", + "schema": "public", + "table": "forum_topics", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_articles_pkey", + "schema": "public", + "table": "newsroom_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_bulletins_pkey", + "schema": "public", + "table": "newsroom_bulletins", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_categories_pkey", + "schema": "public", + "table": "newsroom_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_comments_pkey", + "schema": "public", + "table": "newsroom_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_events_pkey", + "schema": "public", + "table": "newsroom_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_events_sessions_pkey", + "schema": "public", + "table": "newsroom_events_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_explainers_pkey", + "schema": "public", + "table": "newsroom_explainers", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_explainers_faq_pkey", + "schema": "public", + "table": "newsroom_explainers_faq", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_letters_pkey", + "schema": "public", + "table": "newsroom_letters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_pages_pkey", + "schema": "public", + "table": "newsroom_pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_stories_pkey", + "schema": "public", + "table": "newsroom_stories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "newsroom_tags_pkey", + "schema": "public", + "table": "newsroom_tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "payments_demo_purchases_pkey", + "schema": "public", + "table": "payments_demo_purchases", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + "providerId", + "providerAccountId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_provider_account_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_user_provider_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "issuedById", + "idempotencyKey" + ], + "nullsNotDistinct": false, + "name": "core_moderation_warnings_idempotency_key", + "entityType": "uniques", + "schema": "public", + "table": "core_moderation_warnings" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_admin_sessions_token_unique", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_admin_dashboard_userId_unique", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "core_files_key_unique", + "schema": "public", + "table": "core_files", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_languages_code_unique", + "schema": "public", + "table": "core_languages", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_moderation_report_reasons_code_key", + "schema": "public", + "table": "core_moderation_report_reasons", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "openKey" + ], + "nullsNotDistinct": false, + "name": "core_moderation_reports_openKey_key", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "idempotencyKey" + ], + "nullsNotDistinct": false, + "name": "core_notification_deliveries_idempotencyKey_key", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_passkey_challenges_tokenHash_key", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "credentialId" + ], + "nullsNotDistinct": false, + "name": "core_users_passkeys_credentialId_key", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_sessions_token_unique", + "schema": "public", + "table": "core_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "publicId" + ], + "nullsNotDistinct": false, + "name": "core_sessions_known_devices_publicId_unique", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "stateHash" + ], + "nullsNotDistinct": false, + "name": "core_sso_attempts_stateHash_key", + "schema": "public", + "table": "core_sso_attempts", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_sso_pending_tokenHash_key", + "schema": "public", + "table": "core_sso_pending", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "nameCode" + ], + "nullsNotDistinct": false, + "name": "core_users_nameCode_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "name" + ], + "nullsNotDistinct": false, + "name": "core_users_name_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "core_users_email_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_confirm_emails_token_unique", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_userId_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_token_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_operations_tokenHash_key", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "warningId" + ], + "nullsNotDistinct": false, + "name": "core_moderation_sanctions_warningId_key", + "schema": "public", + "table": "core_moderation_sanctions", + "entityType": "uniques" + }, + { + "value": "\"amount\" > 0", + "name": "core_payments_amount_positive", + "entityType": "checks", + "schema": "public", + "table": "core_payments" + }, + { + "value": "\"intent\" IN ('link', 'import', 'sync', 'preview')", + "name": "core_users_sso_operations_intent_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "value": "\"field\" IN ('avatar', 'firstName', 'lastName')", + "name": "core_users_sso_profile_sources_field_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_profile_sources" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/web/content/docs/content-engine/comments-api.mdx b/apps/web/content/docs/content-engine/comments-api.mdx index 77137ca5e..a885e0d7b 100644 --- a/apps/web/content/docs/content-engine/comments-api.mdx +++ b/apps/web/content/docs/content-engine/comments-api.mdx @@ -58,7 +58,7 @@ const comments = useContentComments({ queryKey, thread, ...actions }) | `liveStatus` | `"off"` (no `live`, or no open WebSocket), `"joining"`, `"live"`, or `"denied"` when the server refused or revoked the subscription | | `trackDraft(id, open)` | Call with `true` while an edit or reply form is open on `id` and `false` when it closes. A live update that removes that comment keeps it on screen with `removed: true` and its record until then. `useContentCommentItem` calls it for you | | `replyCount`, `truncated` | Every public comment at any depth, and whether a page held more replies than one answer carries | -| `permissionsOf(id)` | `{ canDelete, canEdit, canHide, canReport }`, all `false` until known or after a failed batch | +| `permissionsOf(id)` | `{ canDelete, canEdit, canHide, canReport, canWarn }`, all `false` until known or after a failed batch | | `canComment`, `canReply(node)` | Whether the composer and **Reply** show: signed in, `create` given, under `maxDepth`, not hidden | | `features` | Which actions you wired up | | `busy` | The action running right now, `{ action, id }` or `null`. A second action waits for it | diff --git a/apps/web/content/docs/content-engine/public-permissions-api.mdx b/apps/web/content/docs/content-engine/public-permissions-api.mdx index d7e6e3a5f..9b907f21e 100644 --- a/apps/web/content/docs/content-engine/public-permissions-api.mdx +++ b/apps/web/content/docs/content-engine/public-permissions-api.mdx @@ -37,6 +37,7 @@ Cookie: vitnode_auth= | --- | --- | --- | | `canEdit` | `GET /{id}/edit` and `PATCH /{id}` would accept this visitor: a moderator with `can_edit` in the record's container, or the owner while the record is not hidden, `update.own` is on and the container's `authorEdit` policy still allows it | Always `false` | | `canReport` | The content type has `reports`, the record is public, and the visitor did not write it | `true` on public records. The report dialog asks them to sign in | +| `canWarn` | The content type has `reports`, the visitor is a moderator with `can_warn` for the record's scope, and did not write it. See [Warn content authors](/docs/dev/moderation/warn-content) | `false` | A record is **left out of `items`** when the visitor can neither read it publicly nor edit it: a missing id, someone else's draft, or a hidden record for anyone but a moderator with `can_edit`. The route answers the same way for all of them, so it never confirms that a draft exists. Your own draft comes back with `canEdit: true` and `canReport: false`. diff --git a/apps/web/content/docs/content-engine/public-write-api.mdx b/apps/web/content/docs/content-engine/public-write-api.mdx index c35f0fb3a..8cc966571 100644 --- a/apps/web/content/docs/content-engine/public-write-api.mdx +++ b/apps/web/content/docs/content-engine/public-write-api.mdx @@ -74,7 +74,7 @@ The routes live in the public module, under `/api/{pluginId}/content/{publicApi. | `POST /` | `create` | `{ values }` | `201` with the record | | `GET /{id}/edit` | `update` | | `200` with the record, drafts included, `Cache-Control: private, no-store` | | `PATCH /{id}` | `update` | `{ values, expectedVersion }` | `200` with the record | -| `GET /permissions?ids=1,2` | `publicWrite`, `reports` or `visibility` | | `200` with `{ items: [{ id, canDelete, canEdit, canHide, canReport }] }`, `Cache-Control: private, no-store` | +| `GET /permissions?ids=1,2` | `publicWrite`, `reports` or `visibility` | | `200` with `{ items: [{ id, canDelete, canEdit, canHide, canReport, canWarn }] }`, `Cache-Control: private, no-store` | `GET /{id}/edit` and `PATCH /{id}` answer only for the owner and for moderators. There is no public delete, publish or hide route. diff --git a/apps/web/content/docs/dev/audit-logs/index.mdx b/apps/web/content/docs/dev/audit-logs/index.mdx index bb8469092..e0c539ccd 100644 --- a/apps/web/content/docs/dev/audit-logs/index.mdx +++ b/apps/web/content/docs/dev/audit-logs/index.mdx @@ -50,8 +50,8 @@ after, an **Open** link to the target and **Show this person's activity**. | Content Engine | Create, edit, delete, duplicate, publish, unpublish, hide, unhide, restore, schedule and every translation change of every content type | | Roles and staff | Roles created, edited and deleted; administrator and moderator permissions added, changed and removed | | Users | Profile and role changes made in the AdminCP, password resets, email verification, signed-out devices | -| Settings | AI settings and access, notification types and danger-zone actions, report reasons, log retention and log exports | -| Moderation | Taking, assigning, resolving and reopening reports, report notes, acting on reported content, approving and rejecting submissions | +| Settings | AI settings and access, notification types and danger-zone actions, report reasons, warning reasons and thresholds, log retention and log exports | +| Moderation | Taking, assigning, resolving and reopening reports, report notes, acting on reported content, approving and rejecting submissions, [warnings and suspensions](/docs/dev/moderation/warnings) | | Background work | Scheduled publishing, cron jobs and queue tasks, attributed to the system; manual cron runs, attributed to the admin who started them | | Payments | Checkouts started by members, and every status change the provider reports (processing, succeeded, failed, expired) | diff --git a/apps/web/content/docs/dev/events/built-in-events.mdx b/apps/web/content/docs/dev/events/built-in-events.mdx index 90e2989a1..831a80726 100644 --- a/apps/web/content/docs/dev/events/built-in-events.mdx +++ b/apps/web/content/docs/dev/events/built-in-events.mdx @@ -564,11 +564,13 @@ member is notified. ## Moderation events -Eleven names, declared in `VitNodeEvents` in +Twenty names, declared in `VitNodeEvents` in `packages/vitnode/src/api/models/events.ts` and emitted by core with `pluginId: "@vitnode/core"`. Each fires **after** its database write commits. The envelope's `actor` is the reporter for `created`, the -administrator for `report_reason.*` and the moderator for the rest. Adding an internal note emits nothing. See +administrator for `report_reason.*`, `warning_reason.*` and +`warning_threshold.*`, and the moderator for the rest. No payload carries a +staff note. Adding an internal note emits nothing. See [Moderation](/docs/dev/moderation). | Event | Payload | Fires when | @@ -584,6 +586,15 @@ administrator for `report_reason.*` and the moderator for the rest. Adding an in | `moderation.report_reason.updated` | `{ reasonId, code }` | An admin edits a report reason | | `moderation.report_reason.deleted` | `{ reasonId, code }` | An admin deletes a report reason. Its reports stay | | `moderation.report_reason.reordered` | `{ reasonIds }` | An admin saves a new order. `reasonIds` lists every reason in that order | +| `moderation.warning.issued` | `{ warningId, userId, issuedById, points, reasonId, reportId, pluginId, targetType, targetId }` | A moderator issues a [warning](/docs/dev/moderation/warnings). Not emitted again for a retried request | +| `moderation.warning.revoked` | `{ warningId, userId, revokedById }` | A moderator revokes a warning | +| `moderation.sanction.applied` | `{ sanctionId, userId, type, source, endsAt, thresholdId, warningId }` | A threshold suspends an account. `endsAt` is `null` for an indefinite suspension | +| `moderation.sanction.revoked` | `{ sanctionId, userId, revokedById }` | A moderator lifts a suspension | +| `moderation.warning_reason.created` | `{ reasonId }` | An admin adds a warning reason | +| `moderation.warning_reason.updated` | `{ reasonId }` | An admin edits or disables a warning reason | +| `moderation.warning_threshold.created` | `{ thresholdId }` | An admin adds a warning threshold | +| `moderation.warning_threshold.updated` | `{ thresholdId }` | An admin edits, enables or disables a threshold | +| `moderation.warning_threshold.deleted` | `{ thresholdId }` | An admin deletes a threshold | A hide or restore from the ModCP also emits the content type's own `content.{id}.hidden` or `unhidden` event, because it goes through the same diff --git a/apps/web/content/docs/dev/moderation/configure-warnings.mdx b/apps/web/content/docs/dev/moderation/configure-warnings.mdx new file mode 100644 index 000000000..3780b7f4c --- /dev/null +++ b/apps/web/content/docs/dev/moderation/configure-warnings.mdx @@ -0,0 +1,82 @@ +--- +title: Configure warnings +description: Set up VitNode warning reasons with default points and expiry, and warning thresholds that suspend an account automatically, in the AdminCP. +icon: Settings2 +--- + +import { ImgDocs } from "@/components/fumadocs/img"; +import reasons from "./warning-reasons.webp"; +import thresholds from "./warning-thresholds.webp"; + +Warning reasons give moderators consistent points and messages, and thresholds decide when points turn into a suspension. This guide sets up the example from [Warnings](/docs/dev/moderation/warnings): a 2-point **Spam** reason and suspensions at 8 and 10 points. + +## Before you begin + +You need an AdminCP account with the **Warning settings** permissions: **View warning reasons and thresholds**, and **Manage warning reasons and thresholds** to change them. Admins with a root role have both. + + + + + +### Add a warning reason + +Open **AdminCP → Moderation → Warning reasons** and choose **Add reason**. Fill in: + +| Field | Example | What it does | +| --- | --- | --- | +| Title | Spam | What moderators pick and what the member reads | +| Description | Ads or repeated junk | Helps moderators choose; members never see it | +| Points | 2 | Added to the member's active points. `0` makes an official warning without points | +| Points expire after | 30 days | After this the points stop counting. Check **Never expire** for permanent points | +| Default message | Please stop posting spam. | Prefilled in the warn form; moderators can edit it | +| Enabled | On | Disabled reasons can't be picked for new warnings | + +Title, description and message are translated per language, like report reasons. Drag rows to set the order moderators see. + + + +Reasons are never deleted, only disabled. Every warning keeps a copy of its reason's title and the points it was issued with, so editing or disabling a reason never changes a warning already issued. + + + + + +### Add thresholds + +Open **AdminCP → Moderation → Warning thresholds** and choose **Add threshold** twice: + +| Active points | Action | Duration | +| --- | --- | --- | +| 8 | Temporary suspension | 24 hours | +| 10 | Indefinite suspension | None | + + + +Only one enabled threshold can use each number of points; saving a second one answers **Another enabled threshold already uses this number of points**. Disable or delete a threshold to stop it. Suspensions it already created keep a copy of the rule and stay until they end or are lifted. + + + + + +## Check the result + +Warn a test member twice with **Spam** (4 points), then preview a third time in the warn form: **Automatic action** says nothing. Change the points to 4 (with an explanation) and the preview shows **Account suspended for 1 day**. See [Issue warnings](/docs/dev/moderation/issue-warnings). + +Changes apply to warnings issued from now on. Saving a lower threshold does not suspend members who are already above it, and raising one does not lift anything. + +Every change to a reason or threshold, with its values before and after, is recorded in **AdminCP → Staff → Administrators → Logs**. + +## Related + +- [Warnings](/docs/dev/moderation/warnings) +- [Configure moderators](/docs/dev/moderation/configure-moderators) diff --git a/apps/web/content/docs/dev/moderation/index.mdx b/apps/web/content/docs/dev/moderation/index.mdx index adbc7ad92..0424b45cb 100644 --- a/apps/web/content/docs/dev/moderation/index.mdx +++ b/apps/web/content/docs/dev/moderation/index.mdx @@ -24,6 +24,7 @@ A moderator signs in with their normal user session. The ModCP never asks for an | Give a user or role moderator permissions in the AdminCP | [Configure moderators](/docs/dev/moderation/configure-moderators) | | Review, assign, resolve and act on reports | [Process reports](/docs/dev/moderation/process-reports) | | Approve or reject new content and edits before they go live | [Review approvals](/docs/dev/moderation/review-approvals) | +| Warn members and suspend accounts automatically at a points limit | [Warnings](/docs/dev/moderation/warnings) | | Look up routes, options, events and limits | [Moderation reference](/docs/dev/moderation/reference) | ## How the parts fit together @@ -37,4 +38,4 @@ Reports belong to core, not to the plugin. They keep their snapshot after the ar ## What is not included yet -This version covers reports, hiding and restoring content, and [reviewing approvals](/docs/dev/moderation/review-approvals). It has no warnings, bans, automatic moderation, anonymous reports, report notifications or permanent deletion from the ModCP. Moderators still have to read things themselves, sorry. +This version covers reports, hiding and restoring content, [reviewing approvals](/docs/dev/moderation/review-approvals) and [warnings with automatic suspensions](/docs/dev/moderation/warnings). It has no manual bans, appeals, automatic content moderation, anonymous reports, report notifications or permanent deletion from the ModCP. Moderators still have to read things themselves, sorry. diff --git a/apps/web/content/docs/dev/moderation/issue-warnings.mdx b/apps/web/content/docs/dev/moderation/issue-warnings.mdx new file mode 100644 index 000000000..32fc65909 --- /dev/null +++ b/apps/web/content/docs/dev/moderation/issue-warnings.mdx @@ -0,0 +1,114 @@ +--- +title: Issue warnings +description: Warn a VitNode member from their profile, a comment, a report or the Moderator CP, preview the consequences, and revoke warnings or lift suspensions. +icon: TriangleAlert +--- + +import { ImgDocs } from "@/components/fumadocs/img"; +import form from "./warning-form.webp"; +import member from "./warning-member.webp"; +import memberLog from "./warning-member-log.webp"; + +Moderators warn members with one shared form, opened from wherever the problem is. This guide warns a member for a blog comment, then revokes the warning and lifts the suspension it caused. + +## Before you begin + +Warnings need [warning reasons](/docs/dev/moderation/configure-warnings) and a moderator entry with the right permissions. Grant them in **AdminCP → Staff → Moderators**: + +| Permission | Where | Lets the moderator | +| --- | --- | --- | +| **Warn authors of this content** (`can_warn`) | The content type's module, for example **Comment** in the blog. Can be [scoped](/docs/dev/moderation/scoped-permissions) | Warn the author of that content | +| **Warn members directly, without content** | Core → **Warnings** | Warn from a profile or the ModCP without content | +| **Change a warning's points** / **Change when a warning's points expire** | Core → **Warnings** | Override the reason's defaults, with an explanation | +| **View every member's warnings** | Core → **Warnings** | See all warnings, not only those in their scope, and a member's **Moderation log** | +| **Read staff notes on warnings** | Core → **Warnings** | Read private notes and override explanations | +| **Revoke warnings** | Core → **Warnings** | Revoke a warning | +| **View suspensions** / **Lift suspensions** | Core → **Warnings** | See and lift suspensions | + +A moderator limited to one forum section can warn for content in that section only. Without **Warn members directly** they cannot warn from a profile, and without **View every member's warnings** they see only the warnings they issued or that belong to their scope. The member's total of active points is always shown, because a warning anywhere counts toward account-wide thresholds. + + + + + +### Open the warn form + +Pick the entry point that matches what you are looking at: + +| From | How | +| --- | --- | +| A comment | **More actions** (`…`) → **Warn author** | +| A report | **Warn author** under the status button in the ModCP report | +| A profile | **Warn** next to **Warnings** on the member's profile | +| The ModCP | **Moderator CP → Warnings → Find a member**, then **Warn member** | + +Each button appears only when the server says you may use it. + + + + + +### Fill in the warning + +Pick a reason. The points, expiry and message follow the reason; change the points or expiry only if you may, and say why in **Why the change?**. + +- **Message to the member** is what the member reads in the notification and on their account status page. +- **Staff note** is private. The member never sees it, and only staff who can read notes do. + +**What happens** shows the member's active points now, this warning's points, the total after and the automatic action. It is a preview: the server calculates everything again when you submit, in case someone else warned the member meanwhile. + + + +From a report, **Also close the report as action taken** closes it in the same step. If either part fails, neither happens. Without it, the report stays open and its timeline records the warning. + + + + + +### Submit + +Choose **Issue warning**. The toast says how many active points the member has now and whether their account was suspended, from the server's actual result. If that differs from the preview, the toast says so. Sending the same form twice, for example after a network error, issues it once. + + + + + +### Revoke a warning or lift a suspension + +Open the member in **Moderator CP → Warnings** (or **Warnings** on their profile). **Revoke** on a warning asks for a reason; its points stop counting at once and the member is notified. The warning stays in the history, marked **Revoked**. + +Revoking a warning never lifts the suspension it caused. Lift it separately with **Lift** on the suspension. Other active suspensions keep applying. + +The **Moderation log** at the bottom of the page lists who warned the member, revoked a warning or applied and lifted a suspension, newest first, with points before and after. It needs **View every member's warnings**, and suspension entries also need **View suspensions**. + + + + + + + + + +## Check the result + +Sign in as the warned member. A notification links to **Account status**, which lists the warning with your message and its points. If the warning crossed a threshold, every page shows the suspension notice and actions such as commenting answer like a signed-out visitor's. + +## Related + +- [Warnings](/docs/dev/moderation/warnings) +- [Process reports](/docs/dev/moderation/process-reports) diff --git a/apps/web/content/docs/dev/moderation/meta.json b/apps/web/content/docs/dev/moderation/meta.json index fa3c4e79a..3e93372d2 100644 --- a/apps/web/content/docs/dev/moderation/meta.json +++ b/apps/web/content/docs/dev/moderation/meta.json @@ -1,6 +1,6 @@ { "title": "Moderation", - "description": "Reports, the Moderator CP and scoped moderator permissions", + "description": "Reports, warnings, the Moderator CP and scoped moderator permissions", "icon": "ShieldAlert", "pages": [ "index", @@ -10,6 +10,10 @@ "configure-moderators", "process-reports", "review-approvals", + "warnings", + "configure-warnings", + "issue-warnings", + "warn-content", "reference" ] } diff --git a/apps/web/content/docs/dev/moderation/reference.mdx b/apps/web/content/docs/dev/moderation/reference.mdx index c31ff51a2..e779e747a 100644 --- a/apps/web/content/docs/dev/moderation/reference.mdx +++ b/apps/web/content/docs/dev/moderation/reference.mdx @@ -223,12 +223,83 @@ Statuses are `pending`, `approved`, `rejected`, `superseded` (replaced by the au | `moderation.report_reason.updated` | `{ reasonId, code }` | | `moderation.report_reason.deleted` | `{ reasonId, code }` | | `moderation.report_reason.reordered` | `{ reasonIds }` in the new order | +| `moderation.warning.issued` | `{ warningId, userId, issuedById, points, reasonId, reportId, pluginId, targetType, targetId }` | +| `moderation.warning.revoked` | `{ warningId, userId, revokedById }` | +| `moderation.sanction.applied` | `{ sanctionId, userId, type, source, endsAt, thresholdId, warningId }` | +| `moderation.sanction.revoked` | `{ sanctionId, userId, revokedById }` | +| `moderation.warning_reason.created` / `.updated` | `{ reasonId }` | +| `moderation.warning_threshold.created` / `.updated` / `.deleted` | `{ thresholdId }` | | `content.approval.submitted` | `{ submissionId, replacedSubmissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId }` | | `content.approval.approved` | `{ submissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId, reviewerId }` | | `content.approval.rejected` | `{ submissionId, pluginId, contentTypeId, contentId, containerId, kind, authorId, reviewerId, reason }` | Adding a note emits no event. Hiding through a report also emits the content type's own `hidden` or `unhidden` event. See [Built-in events](/docs/dev/events/built-in-events#moderation-events). +## Warnings + +Concepts and flows are in [Warnings](/docs/dev/moderation/warnings). Everything below is core's. + +### Warning permissions + +| Type | Module | Permission | Scoped | +| --- | --- | --- | --- | +| Moderator | A report target's `permissionModule` | `can_warn`: warn the content's authors. Added only for targets with `resolveAuthors` | With the target's `scope` | +| Moderator | `warnings` | `can_view`, `can_warn` (direct), `can_override_points`, `can_override_expiration`, `can_view_notes`, `can_revoke`, `can_view_sanctions`, `can_manage_sanctions` | No | +| Admin | `warning_settings` | `can_view`, `can_manage` | No | + +### Warning API routes + +Under `/api/@vitnode/core/moderation`, using the user session. + +| Method and path | Who | Body or query | +| --- | --- | --- | +| `GET /warnings/reasons` | Anyone who can warn | Enabled reasons in display order | +| `POST /warnings/preview` | Anyone who can warn | `{ userId, reasonId, points?, expiration?, content?, reportId? }`. Runs every check and answers points before and after and the projected `sanction` | +| `POST /warnings` | Anyone who can warn | The preview body plus `{ message, staffNote?, overrideNote?, resolveReport?, idempotencyKey }` | +| `POST /warnings/{id}/revoke` | `can_revoke` | `{ reason }` | +| `GET /warnings` | Moderators | Query `userId`, `reasonId`, `issuedById`, `status`, `pluginId`, `targetType`, `from`, `to`, `page`, `pageSize` | +| `GET /members?search=` | `can_view` or `can_warn` | Up to 10 members | +| `GET /members/{id}` | `can_view` or anyone who can warn | Active points, visible warnings, `hiddenWarnings`, sanctions with `can_view_sanctions`, and `permissions.viewLog` | +| `GET /members/{id}/log` | `can_view` | The member's moderation log from the Activity Logs, newest first, 20 per page with `cursor`. `sanction.*` entries need `can_view_sanctions` | +| `POST /sanctions/{id}/revoke` | `can_manage_sanctions` | `{ reason }` | +| `GET /account-status` | The member, also while suspended | Own suspension, active points, warnings and suspensions, never staff notes | + +`expiration` is `{ mode: "default" }`, `{ mode: "never" }`, `{ mode: "hours", hours }` or `{ mode: "at", at }` (an ISO date in the future). `content` is `{ pluginId, targetType, targetId }`; with `reportId`, the content comes from the report. A retry with the same `idempotencyKey` from the same moderator answers the first result with `duplicate: true`. + +Refusals answer `{ error }`: `self` and `protected` (`403`), `forbidden` (`403`), `points_override_forbidden` and `expiration_override_forbidden` (`403`), `override_note_required`, `invalid_points`, `invalid_expiration`, `author_mismatch` and `report_mismatch` (`400`), `user_not_found`, `reason_not_found` and `content_not_found` (`404`), `reason_disabled`, `already_revoked` and `report_closed` (`409`). + +The AdminCP routes are under `/api/@vitnode/core/admin/warnings`: `GET`/`POST /reasons`, `PATCH /reasons/{id}`, `PUT /reasons/reorder`, `GET`/`POST /thresholds`, `PUT`/`DELETE /thresholds/{id}`. A second enabled threshold at the same points answers `409 duplicate_points`. + +### Warning limits + +| Limit | Value | +| --- | --- | +| Points per warning or reason | 0 to 100 | +| Threshold points | 1 to 1000 | +| Expiry and suspension length | 1 hour to 5 years | +| Message to the member | 3 to 2000 characters | +| Staff note, explanations, revocation reasons | Up to 2000 characters; explanations and reasons at least 3 | +| Warning reasons / thresholds | 50 / 20 | + +### Warning activity log actions + +Recorded under `@vitnode/core` in `core_audit_logs`, in the same transaction as the change. See [Activity Logs](/docs/dev/audit-logs). + +| Action | Area | Target | Metadata | +| --- | --- | --- | --- | +| `warning.issued` | `moderator` | `user`, the warned member | `warningId`, `points`, `defaultPoints`, `pointsBefore`, `pointsAfter`, `pointsOverridden`, `expirationOverridden`, `expiresAt`, `reasonId`, `reportId`, the content's `contentPluginId`/`contentType`/`contentId`, `crossedThresholdIds`, `appliedThresholdId` | +| `warning.revoked` | `moderator` | `user` | `warningId`, `points` | +| `sanction.applied` | `moderator` | `user` | `sanctionId`, `warningId`, `thresholdId`, `thresholdPoints`, `thresholdAction`, `endsAt`, `source` | +| `sanction.revoked` | `moderator` | `user` | `sanctionId`, `warningId`, `type` | +| `warning_reason.created` / `.updated` / `.reordered` | `admin` | `warning_reason` | Changes of points, expiry, enabled and title | +| `warning_threshold.created` / `.updated` / `.deleted` | `admin` | `warning_threshold` | Changes of points, action, duration and enabled | + +A content-linked warning carries the content's moderation scope. Staff notes, override explanations and revocation reasons are never recorded. + +### Warning notifications + +`core.warning_issued`, `core.warning_revoked`, `core.account_suspended` and `core.suspension_revoked`, in the **Account** category. They are mandatory in the inbox, link to `/account-status`, and are published inside the warning's transaction, so a rolled-back warning notifies nobody. They carry the member's message and points, never staff notes or the moderator's name. + ## Snapshot format Stored in `core_moderation_reports.snapshot` as JSON, `version: 1`: @@ -274,4 +345,6 @@ The migration `20261011193321_add_moderation_reports` adds `core_moderation_repo The migration `20261019161102_add_moderation_report_reasons` adds `core_moderation_report_reasons` with a unique `code`, `requiresDetails` and `position`. Titles and descriptions live in `core_languages_words`. `core_moderation_reports.reason` holds the reason's `code` without a foreign key, so deleting a reason keeps its reports. `db:prepare` and `db:migrate` add the four starter reasons only while the table is empty. +The migration `20261030193055_add_moderation_warnings` adds `core_moderation_warning_reasons`, `core_moderation_warning_thresholds` (a partial unique index allows one enabled threshold per number of points), `core_moderation_warnings` and `core_moderation_sanctions`. Their history is in `core_audit_logs`. A warning keeps a copy of its reason's title, the points it was issued with and a short content snapshot; `(issuedById, idempotencyKey)` is unique, and so is a sanction's `warningId`. Warned users' rows cascade on account deletion; actors are `ON DELETE SET NULL`. + Approvals live in `core_content_submissions`, one row per submission. Rows are never deleted with the content, author and reviewer are `ON DELETE SET NULL`, and a partial unique index allows one `pending` submission per record. Container policy overrides are stored in `core_content_containers.policies`. diff --git a/apps/web/content/docs/dev/moderation/warn-content.mdx b/apps/web/content/docs/dev/moderation/warn-content.mdx new file mode 100644 index 000000000..e998e17fb --- /dev/null +++ b/apps/web/content/docs/dev/moderation/warn-content.mdx @@ -0,0 +1,90 @@ +--- +title: Warn content authors +description: Let VitNode moderators warn the author of your plugin's content, with the server resolving authorship and moderation scope, and add the WarnAuthorButton. +icon: UserX +--- + +A content-linked warning names a piece of content, so moderators limited to one section can warn for that section only. This guide adds **Warn author** to your plugin's content. + +## How the server decides + +Warnings reuse your content's **report target**. The browser sends only the content's `pluginId`, `targetType`, `targetId` and the member's id. The server then: + +1. Finds the report target, and refuses content without one. +2. Resolves the content's moderation scope with the target's `resolveScope`. +3. Checks the moderator's `can_warn` in the target's `permissionModule` for that scope. +4. Reads the current authors with the target's `resolveAuthors`, and refuses the warning unless the member is one of them. + +A client-sent author or scope is never trusted. The warning keeps a short title and URL of the content, so it stays readable after the content is deleted or its plugin is removed. + +## Content Engine types + +A content type with `reports` is warnable with no extra code. Core adds `resolveAuthors`, reading the same author field search and reports use, and adds `can_warn` to the type's moderator permissions next to `can_view_reports`. + +The permissions route answers `canWarn` for each record: + +```json title="GET /api/@vitnode/blog/content/comments/permissions?ids=1" +{ + "items": [ + { + "id": 1, + "canDelete": false, + "canEdit": false, + "canHide": false, + "canReport": true, + "canWarn": true + } + ] +} +``` + +Comments rendered with [`ContentCommentActions`](/docs/content-engine/comments-api) show **Warn author** in the **More actions** menu whenever `canWarn` is true and the thread has `report`. The blog's comments get it this way. + +## Add the button to your own page + +For other content, render `WarnAuthorButton` when the permissions route answers `canWarn`: + +```tsx title="src/pages/topic-page.tsx" +import { contentTarget } from "@vitnode/core/content"; +import { WarnAuthorButton } from "@vitnode/core/tanstack/moderation"; + +import { topicContentType } from "@/content/topic"; +import { manifest } from "@/manifest.gen"; + +{permissions.canWarn && topic.author ? ( + +) : null} +``` + +`WarnDialog` takes the same props plus `open` and `onOpenChange`, for a menu item or a trigger you already have. Both accept `reportId` to link the warning to a report, and `onIssued` to refresh your own data with the server's result. + +## Custom report targets + +For a target built with `defineReportTarget`, add `resolveAuthors`. It runs on the server as the moderator, so hidden content still resolves; return `null` once the content is deleted: + +```ts title="src/moderation/topic-target.ts" +import { defineReportTarget } from "@vitnode/core/api/moderation"; + +export const topicTarget = defineReportTarget({ + // ...type, label, permissionModule, parseId, capture, current + resolveAuthors: async (c, { targetId }) => { + const topic = await findTopic(c, targetId); + + return topic ? { authorIds: [topic.authorId], title: topic.title } : null; + }, +}); +``` + +With `resolveAuthors`, the target's module gets a `can_warn` permission. Without it, the target stays reportable but not warnable. + +## Check the result + +Sign in as a moderator with **Warn authors of this content** for your module and open a record written by someone else: **Warn author** appears. Sign in as a moderator without it: the button is gone, and `POST /moderation/warnings` with that content answers `403`. + +## Related + +- [Issue warnings](/docs/dev/moderation/issue-warnings) +- [Custom report targets](/docs/dev/moderation/reference#custom-report-targets) diff --git a/apps/web/content/docs/dev/moderation/warning-form.webp b/apps/web/content/docs/dev/moderation/warning-form.webp new file mode 100644 index 000000000..5092065f4 Binary files /dev/null and b/apps/web/content/docs/dev/moderation/warning-form.webp differ diff --git a/apps/web/content/docs/dev/moderation/warning-member-log.webp b/apps/web/content/docs/dev/moderation/warning-member-log.webp new file mode 100644 index 000000000..cf4b4b378 Binary files /dev/null and b/apps/web/content/docs/dev/moderation/warning-member-log.webp differ diff --git a/apps/web/content/docs/dev/moderation/warning-member.webp b/apps/web/content/docs/dev/moderation/warning-member.webp new file mode 100644 index 000000000..8010adae1 Binary files /dev/null and b/apps/web/content/docs/dev/moderation/warning-member.webp differ diff --git a/apps/web/content/docs/dev/moderation/warning-reasons.webp b/apps/web/content/docs/dev/moderation/warning-reasons.webp new file mode 100644 index 000000000..d9c1186e6 Binary files /dev/null and b/apps/web/content/docs/dev/moderation/warning-reasons.webp differ diff --git a/apps/web/content/docs/dev/moderation/warning-thresholds.webp b/apps/web/content/docs/dev/moderation/warning-thresholds.webp new file mode 100644 index 000000000..dafbfe102 Binary files /dev/null and b/apps/web/content/docs/dev/moderation/warning-thresholds.webp differ diff --git a/apps/web/content/docs/dev/moderation/warnings.mdx b/apps/web/content/docs/dev/moderation/warnings.mdx new file mode 100644 index 000000000..9744d2518 --- /dev/null +++ b/apps/web/content/docs/dev/moderation/warnings.mdx @@ -0,0 +1,56 @@ +--- +title: Warnings +description: How VitNode warnings, active points, thresholds and account suspensions fit together, who can do what, and what a suspended member sees. +icon: TriangleAlert +--- + +A **warning** records a moderator's decision about a member. It can carry **points**, and when a warning brings a member's **active points** up to an administrator's **threshold**, VitNode suspends the account automatically. Warnings, points and suspensions are separate records, so revoking one never silently rewrites another. + +| Concept | What it is | +| --- | --- | +| Warning | Who was warned, by whom, why, how many points, the message the member reads, a private staff note, and optionally the content or report it is about | +| Active points | The sum of points on warnings that are not revoked and whose expiry is still in the future, computed from timestamps on every read | +| Threshold | An administrator's rule, like "at 8 active points, suspend for 24 hours" | +| Sanction | A consequence on the account. This version has temporary and indefinite **suspensions** | + +## Choose a guide + +| You want to | Read | +| --- | --- | +| Set up warning reasons and automatic suspensions | [Configure warnings](/docs/dev/moderation/configure-warnings) | +| Warn a member, revoke a warning or lift a suspension | [Issue warnings](/docs/dev/moderation/issue-warnings) | +| Add **Warn author** to your plugin's content | [Warn content authors](/docs/dev/moderation/warn-content) | +| Look up routes, permissions and events | [Moderation reference](/docs/dev/moderation/reference#warnings) | + +## How a warning becomes a suspension + +1. A moderator opens the warn form. It previews the member's active points before and after, and the suspension a threshold would apply. +2. On submit, the server checks every permission again and locks that member's warnings for the length of one transaction. Two moderators warning the same member at once are handled one after the other, so the points add up and each threshold fires once. +3. A threshold applies when the warning moves active points from below its number to its number or above. When one warning crosses several, only the highest applies: 6 → 11 points with thresholds at 8 and 10 gives only the 10-point suspension. +4. The warning, the threshold decision, the suspension, the [activity log](/docs/dev/audit-logs) entries and the member's notifications commit together or not at all. + +A temporary suspension ends at its own end time, even when the member still has the points. Expired points or a changed threshold never create a new suspension on their own; only a new warning does. Points that later fall below a threshold let it fire again on a future crossing. + +## What a suspended member sees + +The server checks suspensions when it reads the session, for every request: core routes, plugin routes, server functions and WebSocket handshakes. The API treats a suspended account as a guest everywhere, so it can browse public pages but cannot post, comment, report, moderate or open the AdminCP. Applying a suspension also closes the member's open WebSocket connections. + +The member keeps their session. A strip under the header says the account is suspended and links to **Account status** at `/account-status`, which shows the suspension end time, active points and the warning history with each message. Members reach the same page from **Settings → Account status** at any time. It never shows staff notes or which moderator issued a warning. + +## Where the history lives + +Every warning, revocation and suspension is written to the [Activity Logs](/docs/dev/audit-logs), in the same transaction as the change. Administrators see them in **AdminCP → Staff → Moderators → Logs**. Moderators with **View every member's warnings** see one member's entries as the **Moderation log** on that member's ModCP page. The entries hold points, threshold decisions and links to reports, never staff notes, override explanations or revocation reasons. Changes to warning reasons and thresholds land in the administrators' log. + +## Who is protected + +Nobody can warn themselves. A member who holds a root role cannot be warned at all, and other staff (admins and moderators) can only be warned by someone who holds a root role. These checks run before a threshold is evaluated, so a threshold can never suspend a protected account. + +## Limits of this version + +- Suspensions are the only sanction. There is no manual "suspend without a warning" action, no appeal workflow and no posting restriction per plugin yet; the data model leaves room for them. +- Suspension state is cached for up to a minute per member. Applying or lifting a suspension clears that cache, and with Redis every instance sees it at once. Without Redis, other instances may take up to a minute. + +## Related + +- [Scoped permissions](/docs/dev/moderation/scoped-permissions) +- [Configure moderators](/docs/dev/moderation/configure-moderators) diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index 8e1143530..2ccdea3b6 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -36,6 +36,8 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/admin/core/debug/services' }, { path: '/admin/core/debug/vitnode' }, { path: '/admin/core/moderation/report-reasons' }, + { path: '/admin/core/moderation/warning-reasons' }, + { path: '/admin/core/moderation/warning-thresholds' }, { path: '/admin/core/staff/admins' }, { path: '/admin/core/staff/moderators' }, { path: '/admin/core/system/files' }, @@ -57,6 +59,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/forum/topics/:slug', maxAge: 60, staleWhileRevalidate: 30 }, { path: '/login/sso/:providerId' }, { path: '/modcp/approvals/:id' }, + { path: '/modcp/members/:id' }, { path: '/modcp/reports/:id' }, { path: '/news/comments/:slug' }, { path: '/newsroom/guide/:topic' }, @@ -69,6 +72,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/login/verify-email' }, { path: '/modcp/approvals' }, { path: '/modcp/reports' }, + { path: '/modcp/warnings' }, { path: '/news/new' }, { path: '/newsroom/browse' }, { path: '/newsroom/embed' }, @@ -96,6 +100,7 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/stories/:slug' }, { path: '/users/:nameCode' }, { path: '/solutions/:slug' }, + { path: '/account-status' }, { path: '/admin' }, { path: '/blog', maxAge: 300, staleWhileRevalidate: 60 }, { path: '/desks' }, diff --git a/packages/vitnode/src/api/lib/moderation/registry.ts b/packages/vitnode/src/api/lib/moderation/registry.ts index 389fdf3a1..b67779c4b 100644 --- a/packages/vitnode/src/api/lib/moderation/registry.ts +++ b/packages/vitnode/src/api/lib/moderation/registry.ts @@ -30,7 +30,10 @@ export interface ModerationRegistry { } export const moderationPermissionEntries = ( - target: Pick, + target: Pick< + ModerationReportTargetDefinition, + "actions" | "resolveAuthors" | "scope" + >, ): PermissionStaffEntryInput[] => { const scope = target.scope ? { scope: target.scope } : {}; const hideable = Boolean(target.actions?.hide ?? target.actions?.unhide); @@ -52,6 +55,9 @@ export const moderationPermissionEntries = ( permission: MODERATION_PERMISSIONS.deleteNotes, ...scope, }, + ...(target.resolveAuthors + ? [{ permission: MODERATION_PERMISSIONS.warn, ...scope }] + : []), ...(hideable ? [ { permission: MODERATION_PERMISSIONS.viewHidden, ...scope }, diff --git a/packages/vitnode/src/api/lib/moderation/targets.ts b/packages/vitnode/src/api/lib/moderation/targets.ts index f9ce8dab3..aec4d7a7e 100644 --- a/packages/vitnode/src/api/lib/moderation/targets.ts +++ b/packages/vitnode/src/api/lib/moderation/targets.ts @@ -14,6 +14,14 @@ export interface ModerationTargetState { url: null | string; } +/** What {@link ModerationReportTargetDefinition.resolveAuthors} returns. */ +export interface ModerationTargetAuthors { + /** Every user who wrote the target; a warning can only name one of them. */ + authorIds: number[]; + /** A short plain-text title kept with the warning, or `null`. */ + title: null | string; +} + export interface ModerationCaptureArgs { locale: null | string; targetId: string; @@ -84,6 +92,17 @@ export interface ModerationReportTargetDefinition { parseId: (value: string) => null | string; /** Moderator permission module the report permissions are added to. */ permissionModule: string; + /** + * The target's current authors and a short plain-text title, or `null` once + * it is deleted. Server-only; runs as the moderator, so hidden content still + * resolves. Setting it makes the target warnable: moderators with + * `can_warn` in `permissionModule` (limited by `scope`) can warn its + * authors. Content Engine types with `reports` provide it automatically. + */ + resolveAuthors?: ( + c: Context, + args: { targetId: string }, + ) => Promise; /** * The target's resource id in the `scope` family: `""` for none, * `undefined` once the target is deleted. Required with `scope`. diff --git a/packages/vitnode/src/api/lib/moderation/warnings/access.ts b/packages/vitnode/src/api/lib/moderation/warnings/access.ts new file mode 100644 index 000000000..ff50c1b43 --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/access.ts @@ -0,0 +1,181 @@ +import type { SQL } from "drizzle-orm"; +import type { Context } from "hono"; + +import { and, eq, inArray, isNotNull, or } from "drizzle-orm"; + +import { core_moderation_warnings } from "@/database/warnings"; +import { MODERATION_PERMISSIONS } from "@/lib/moderation/const"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { manifest } from "@/manifest"; + +import type { ResolvedStaffPermissionSet } from "../../permission-staff"; +import type { ModeratorContext } from "../access"; + +import { + getStaffFlags, + resolveStaffPermissions, +} from "../../check-staff-permission"; +import { + hasStaffPermission, + staffPermissionResources, +} from "../../staff-permission"; +import { canOnReport } from "../access"; + +export type WarningPermission = + (typeof WARNING_PERMISSIONS)[keyof typeof WARNING_PERMISSIONS]; + +/** Whether the moderator holds one of core's unscoped `warnings` permissions. */ +export const hasWarningPermission = ( + set: ResolvedStaffPermissionSet, + permission: WarningPermission, +): boolean => + hasStaffPermission(set, { + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + }); + +/** The content a warning points at, as stored on the warning. */ +export interface WarningContentRef { + pluginId: null | string; + scopeId: null | string; + targetType: null | string; +} + +/** Whether the moderator may warn the authors of content in this scope. */ +export const canWarnForContent = ( + c: Context, + set: ResolvedStaffPermissionSet, + ref: { pluginId: string; scopeId: null | string; targetType: string }, +): boolean => canOnReport(c, set, ref, MODERATION_PERMISSIONS.warn); + +/** + * Whether the moderator may read one warning's details: with `can_view`, any + * warning; otherwise the warnings they issued and those linked to content + * they may warn for. + */ +export const canSeeWarning = ( + c: Context, + moderator: ModeratorContext, + warning: WarningContentRef & { issuedById: null | number }, +): boolean => { + if (hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + return true; + } + if (warning.issuedById === moderator.user.id) return true; + if (!warning.pluginId || !warning.targetType) return false; + + return canWarnForContent(c, moderator.set, { + pluginId: warning.pluginId, + scopeId: warning.scopeId, + targetType: warning.targetType, + }); +}; + +/** Staff notes and override explanations: `can_view_notes`, or the issuer's own. */ +export const canSeeWarningNotes = ( + moderator: ModeratorContext, + warning: { issuedById: null | number }, +): boolean => + hasWarningPermission(moderator.set, WARNING_PERMISSIONS.viewNotes) || + warning.issuedById === moderator.user.id; + +/** {@link canSeeWarning} as SQL, for listing. `undefined` means every warning. */ +export const viewableWarningsCondition = ( + c: Context, + moderator: ModeratorContext, +): SQL | undefined => { + if (hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + return undefined; + } + + const table = core_moderation_warnings; + const conditions: (SQL | undefined)[] = [ + eq(table.issuedById, moderator.user.id), + ]; + for (const target of c.get("core").moderation?.targets ?? []) { + if (!target.definition.resolveAuthors) continue; + const family = target.definition.scope; + const resources = staffPermissionResources( + moderator.set, + { + module: target.definition.permissionModule, + permission: MODERATION_PERMISSIONS.warn, + plugin: target.pluginId, + }, + family ?? "", + ); + if (resources === null) continue; + + const ofTarget = and( + eq(table.pluginId, target.pluginId), + eq(table.targetType, target.definition.type), + ); + conditions.push( + resources === "all" || !family + ? ofTarget + : and( + ofTarget, + isNotNull(table.scopeId), + inArray(table.scopeId, resources), + ), + ); + } + + return or(...conditions); +}; + +/** + * Whether the moderator can warn anyone at all: directly, or for content of + * at least one registered target. + */ +export const canWarnAnywhere = ( + c: Context, + set: ResolvedStaffPermissionSet, +): boolean => { + if (hasWarningPermission(set, WARNING_PERMISSIONS.warn)) return true; + + return (c.get("core").moderation?.targets ?? []).some( + target => + target.definition.resolveAuthors !== undefined && + staffPermissionResources( + set, + { + module: target.definition.permissionModule, + permission: MODERATION_PERMISSIONS.warn, + plugin: target.pluginId, + }, + target.definition.scope ?? "", + ) !== null, + ); +}; + +export type WarningTargetRefusal = "protected" | "self"; + +/** + * Who may not be warned by this moderator: themselves, anyone holding a root + * role, and - unless the moderator holds a root role - other staff. Checked + * before threshold evaluation, so a threshold can never suspend a protected + * account. + */ +export const warningTargetRefusal = async ( + c: Context, + moderator: ModeratorContext, + target: { id: number; roleId: number }, +): Promise => { + if (target.id === moderator.user.id) return "self"; + + const targetSet = await resolveStaffPermissions(c, { + type: "moderator", + user: target, + }); + if (targetSet.root) return "protected"; + if (moderator.set.root) return null; + + const flags = await getStaffFlags(c, target); + + return flags.isAdmin || flags.isModerator ? "protected" : null; +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/audit.ts b/packages/vitnode/src/api/lib/moderation/warnings/audit.ts new file mode 100644 index 000000000..66e728d9d --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/audit.ts @@ -0,0 +1,47 @@ +import type { Context } from "hono"; + +import type { AuditDatabase } from "@/api/lib/audit/write"; +import type { AuditScope } from "@/lib/audit/const"; + +import { writeAuditLog } from "@/api/lib/audit/write"; + +/** The audit actions of warnings and sanctions, all about the warned member. */ +export type WarningAuditAction = + | "sanction.applied" + | "sanction.revoked" + | "warning.issued" + | "warning.revoked"; + +/** + * Writes one warning or sanction entry to the central audit log, in the + * moderators' log and in the same transaction as the change. The target is + * the warned member, so their history reads as one moderation log. Never pass + * staff notes, override explanations or revocation reasons in `metadata`. + */ +export const recordWarningAudit = async ( + c: Context, + tx: AuditDatabase, + { + action, + metadata, + scope = null, + user, + }: { + action: WarningAuditAction; + metadata: Record; + scope?: AuditScope | null; + user: { id: number; name: null | string }; + }, +): Promise => { + await writeAuditLog( + c, + { + action, + area: "moderator", + metadata, + scope, + target: { id: user.id, label: user.name, type: "user" }, + }, + tx, + ); +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts new file mode 100644 index 000000000..f021c4222 --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/reasons.ts @@ -0,0 +1,88 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { and, asc, eq, inArray } from "drizzle-orm"; + +import { core_languages_words } from "@/database/languages"; +import { core_moderation_warning_reasons } from "@/database/warnings"; + +export const WARNING_REASON_WORDS = { + pluginCode: "core", + tableName: "core_moderation_warning_reasons", +} as const; + +export type WarningReasonWordVariable = "description" | "message" | "title"; + +const multiLangSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +export const warningReasonSchema = z.object({ + id: z.number(), + points: z.number(), + expiresAfterHours: z.number().nullable(), + enabled: z.boolean(), + position: z.number(), + title: multiLangSchema, + description: multiLangSchema, + message: multiLangSchema, +}); + +export type WarningReason = z.infer; + +/** Every warning reason in display order, with its words; `enabledOnly` for new warnings. */ +export const listWarningReasons = async ( + c: Context, + { enabledOnly = false, ids }: { enabledOnly?: boolean; ids?: number[] } = {}, +): Promise => { + const table = core_moderation_warning_reasons; + const rows = await c + .get("db") + .select({ + enabled: table.enabled, + expiresAfterHours: table.expiresAfterHours, + id: table.id, + points: table.points, + position: table.position, + }) + .from(table) + .where( + and( + enabledOnly ? eq(table.enabled, true) : undefined, + ids ? inArray(table.id, ids.length > 0 ? ids : [0]) : undefined, + ), + ) + .orderBy(asc(table.position), asc(table.id)); + if (rows.length === 0) return []; + + const words = await c + .get("db") + .select({ + itemId: core_languages_words.itemId, + languageCode: core_languages_words.languageCode, + value: core_languages_words.value, + variable: core_languages_words.variable, + }) + .from(core_languages_words) + .where( + and( + eq(core_languages_words.pluginCode, WARNING_REASON_WORDS.pluginCode), + eq(core_languages_words.tableName, WARNING_REASON_WORDS.tableName), + inArray( + core_languages_words.itemId, + rows.map(row => row.id), + ), + ), + ); + const pick = (id: number, variable: WarningReasonWordVariable) => + words + .filter(word => word.itemId === id && word.variable === variable) + .map(word => ({ languageCode: word.languageCode, value: word.value })); + + return rows.map(row => ({ + ...row, + description: pick(row.id, "description"), + message: pick(row.id, "message"), + title: pick(row.id, "title"), + })); +}; diff --git a/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts new file mode 100644 index 000000000..a126bb12b --- /dev/null +++ b/packages/vitnode/src/api/lib/moderation/warnings/suspension.ts @@ -0,0 +1,114 @@ +import type { Context } from "hono"; + +import { and, eq, gt, isNull, or } from "drizzle-orm"; + +import { core_moderation_sanctions } from "@/database/warnings"; +import { isSanctionActiveAt } from "@/lib/moderation/warnings"; + +/** The suspension that keeps an account restricted right now. */ +export interface ActiveSuspension { + /** The latest end among active suspensions; `null` while any is indefinite. */ + endsAt: Date | null; + sanctionIds: number[]; +} + +interface CachedSanction { + endsAt: null | string; + id: number; + startsAt: string; +} + +/** Positive and negative answers both live this long, unless revoked sooner. */ +const SUSPENSION_CACHE_TTL_SECONDS = 60; + +const suspensionCacheKey = (userId: number): string => + `moderation:suspension:${userId}`; + +const loadSuspensions = async ( + c: Context, + userId: number, + now: Date, +): Promise => { + const table = core_moderation_sanctions; + const rows = await c + .get("db") + .select({ + endsAt: table.endsAt, + id: table.id, + startsAt: table.startsAt, + }) + .from(table) + .where( + and( + eq(table.userId, userId), + eq(table.type, "suspension"), + isNull(table.revokedAt), + or(isNull(table.endsAt), gt(table.endsAt, now)), + ), + ); + + return rows.map(row => ({ + endsAt: row.endsAt?.toISOString() ?? null, + id: row.id, + startsAt: row.startsAt.toISOString(), + })); +}; + +/** + * The account's active suspension at server time, or `null`. Each sanction is + * re-checked against its own timestamps on every call, so a temporary one + * stops applying the moment it ends - no scheduled job involved. Cached per + * user for a minute and dropped by {@link forgetSuspension} whenever a + * sanction is applied or revoked. Server-only. + */ +export const findActiveSuspension = async ( + c: Context, + userId: number, + now: Date = new Date(), +): Promise => { + const cache = c.get("cache"); + const key = suspensionCacheKey(userId); + let sanctions = await cache.getSystem(key); + if (!sanctions) { + sanctions = await loadSuspensions(c, userId, now); + await cache.setSystem(key, sanctions, SUSPENSION_CACHE_TTL_SECONDS); + } + + const active = sanctions.filter(sanction => + isSanctionActiveAt( + { + endsAt: sanction.endsAt ? new Date(sanction.endsAt) : null, + revokedAt: null, + startsAt: new Date(sanction.startsAt), + }, + now, + ), + ); + if (active.length === 0) return null; + + const indefinite = active.some(sanction => sanction.endsAt === null); + const latest = active + .map(sanction => sanction.endsAt) + .filter((value): value is string => value !== null) + .sort() + .at(-1); + + return { + endsAt: indefinite || !latest ? null : new Date(latest), + sanctionIds: active.map(sanction => sanction.id), + }; +}; + +/** + * Drops the cached suspension state of a user and closes their open + * WebSocket connections, so a new or revoked sanction applies on the next + * request on every instance that shares the cache. + */ +export const forgetSuspension = async ( + c: Context, + userId: number, + { disconnect }: { disconnect: boolean }, +): Promise => { + await c.get("cache").deleteSystem(suspensionCacheKey(userId)); + if (disconnect) c.get("realtime").closeUserConnections(userId); +}; diff --git a/packages/vitnode/src/api/lib/notifications/core-types.ts b/packages/vitnode/src/api/lib/notifications/core-types.ts index 1fd6ee249..f166cce1e 100644 --- a/packages/vitnode/src/api/lib/notifications/core-types.ts +++ b/packages/vitnode/src/api/lib/notifications/core-types.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { resolveLangValue } from "@/lib/helpers/multi-lang"; + import { buildNotificationType } from "./registry"; export const adminMessageNotification = buildNotificationType({ @@ -16,4 +18,134 @@ export const adminMessageNotification = buildNotificationType({ present: ({ data }) => ({ body: data.description, title: data.title }), }); -export const coreNotificationTypes = [adminMessageNotification]; +const reasonTextSchema = z + .array(z.object({ languageCode: z.string(), value: z.string() })) + .nullable(); + +const ACCOUNT_STATUS_PATH = "/account-status"; + +const utcDateTimeFormats = new Map(); + +/** Emails have no viewer time zone, so suspension ends are written in UTC. */ +const utcDateTimeFormat = (locale: string): Intl.DateTimeFormat => { + let format = utcDateTimeFormats.get(locale); + if (!format) { + format = new Intl.DateTimeFormat(locale, { + dateStyle: "medium", + timeStyle: "short", + timeZone: "UTC", + }); + utcDateTimeFormats.set(locale, format); + } + + return format; +}; + +/** Sent to a member when a moderator warns them. Never carries staff notes. */ +export const warningIssuedNotification = buildNotificationType({ + id: "core.warning_issued", + version: 1, + schema: z.object({ + message: z.string().max(2000), + points: z.number().int().min(0), + reason: reasonTextSchema, + warningId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.warning_issued.label", + description: "core.notifications.types.warning_issued.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ data, locale, t }) => { + const reason = data.reason ? resolveLangValue(data.reason, { locale }) : ""; + + return { + body: data.message, + target: ACCOUNT_STATUS_PATH, + title: reason + ? t("core.notifications.types.warning_issued.title_reason", { + points: data.points, + reason, + }) + : t("core.notifications.types.warning_issued.title", { + points: data.points, + }), + }; + }, +}); + +/** Sent to a member when a warning of theirs is revoked. */ +export const warningRevokedNotification = buildNotificationType({ + id: "core.warning_revoked", + version: 1, + schema: z.object({ + points: z.number().int().min(0), + reason: reasonTextSchema, + warningId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.warning_revoked.label", + description: "core.notifications.types.warning_revoked.description", + defaults: { email: "none", inApp: true }, + email: true, + mandatory: true, + present: ({ data, t }) => ({ + body: t("core.notifications.types.warning_revoked.body", { + points: data.points, + }), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.warning_revoked.title"), + }), +}); + +/** Sent to a member whose account a threshold suspended. */ +export const accountSuspendedNotification = buildNotificationType({ + id: "core.account_suspended", + version: 1, + schema: z.object({ + endsAt: z.string().nullable(), + sanctionId: z.number().int(), + }), + category: "account", + label: "core.notifications.types.account_suspended.label", + description: "core.notifications.types.account_suspended.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ data, locale, t }) => ({ + body: data.endsAt + ? t("core.notifications.types.account_suspended.body_until", { + endsAt: utcDateTimeFormat(locale).format(new Date(data.endsAt)), + }) + : t("core.notifications.types.account_suspended.body_indefinite"), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.account_suspended.title"), + }), +}); + +/** Sent to a member when staff lift one of their suspensions. */ +export const suspensionRevokedNotification = buildNotificationType({ + id: "core.suspension_revoked", + version: 1, + schema: z.object({ sanctionId: z.number().int() }), + category: "account", + label: "core.notifications.types.suspension_revoked.label", + description: "core.notifications.types.suspension_revoked.description", + defaults: { email: "immediate", inApp: true }, + email: true, + mandatory: true, + present: ({ t }) => ({ + body: t("core.notifications.types.suspension_revoked.body"), + target: ACCOUNT_STATUS_PATH, + title: t("core.notifications.types.suspension_revoked.title"), + }), +}); + +export const coreNotificationTypes = [ + adminMessageNotification, + warningIssuedNotification, + warningRevokedNotification, + accountSuspendedNotification, + suspensionRevokedNotification, +]; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 21271b598..88905bf3b 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -19,6 +19,7 @@ import { runAfterResponse } from "@/api/lib/after-response"; import { DEFAULT_SESSION_COOKIE_NAME } from "@/api/lib/auth-cookie"; import { CacheModel } from "@/api/lib/cache"; import { MemoryCacheStore } from "@/api/lib/cache-memory"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; import { AIModel } from "@/api/models/ai"; import { AuditModel } from "@/api/models/audit"; import { EmailModel } from "@/api/models/email"; @@ -295,6 +296,17 @@ export interface EnvVariablesVitNode { search: SearchModel; /** Uploads, reads and deletes files through the storage adapter: see {@link StorageModel}. */ storage: StorageModel; + /** + * Set when the session belongs to a suspended account. `user` is then + * `null`, so every route - core's and every plugin's - treats the request as + * a guest's. Only routes built for the suspended experience (the account + * status page, sign-out) read this. + */ + suspension: null | { + /** The latest end among active suspensions; `null` while any is indefinite. */ + endsAt: Date | null; + userId: number; + }; /** The signed-in user from the session cookie, or `null` for a guest. */ user: null | { avatarColor: string; @@ -739,8 +751,17 @@ export const globalMiddleware = ({ await ensureContentLocalizationLanguages(c, contentTypesMetadata); } - const user = await new SessionModel(c).getUser(); - c.set("user", user); + const sessionUser = await new SessionModel(c).getUser(); + const suspension = sessionUser + ? await findActiveSuspension(c, sessionUser.id) + : null; + c.set("user", suspension ? null : sessionUser); + c.set( + "suspension", + sessionUser && suspension + ? { endsAt: suspension.endsAt, userId: sessionUser.id } + : null, + ); c.set("admin", null); c.set("adminSessionExpiresAt", null); c.set("log", loggerMiddleware(c)); @@ -775,6 +796,9 @@ export const globalAdminMiddleware = () => { extend: c.req.query("passive") !== "true", }); if (!session) throw new HTTPException(403); + if (await findActiveSuspension(c, session.user.id)) { + throw new HTTPException(403); + } c.set("admin", { user: session.user, }); diff --git a/packages/vitnode/src/api/models/events.ts b/packages/vitnode/src/api/models/events.ts index 40d54d76c..efa4a86ce 100644 --- a/packages/vitnode/src/api/models/events.ts +++ b/packages/vitnode/src/api/models/events.ts @@ -149,6 +149,52 @@ export interface VitNodeEvents { code: string; reasonId: number; }; + "moderation.sanction.applied": { + /** `null` for an indefinite sanction. */ + endsAt: Date | null; + sanctionId: number; + source: "manual" | "threshold"; + thresholdId: null | number; + type: "suspension"; + userId: number; + warningId: null | number; + }; + "moderation.sanction.revoked": { + revokedById: number; + sanctionId: number; + userId: number; + }; + "moderation.warning.issued": { + issuedById: number; + pluginId: null | string; + points: number; + reasonId: null | number; + reportId: null | number; + targetId: null | string; + targetType: null | string; + userId: number; + warningId: number; + }; + "moderation.warning.revoked": { + revokedById: number; + userId: number; + warningId: number; + }; + "moderation.warning_reason.created": { + reasonId: number; + }; + "moderation.warning_reason.updated": { + reasonId: number; + }; + "moderation.warning_threshold.created": { + thresholdId: number; + }; + "moderation.warning_threshold.deleted": { + thresholdId: number; + }; + "moderation.warning_threshold.updated": { + thresholdId: number; + }; "notifications.deleted_all": { items: number; }; diff --git a/packages/vitnode/src/api/moderation.ts b/packages/vitnode/src/api/moderation.ts index 797d8ff71..5cce7251d 100644 --- a/packages/vitnode/src/api/moderation.ts +++ b/packages/vitnode/src/api/moderation.ts @@ -15,5 +15,6 @@ export { type ModerationCaptureResult, type ModerationReportTargetDefinition, type ModerationTargetActionDefinition, + type ModerationTargetAuthors, type ModerationTargetState, } from "./lib/moderation/targets"; diff --git a/packages/vitnode/src/api/modules/admin/admin.module.ts b/packages/vitnode/src/api/modules/admin/admin.module.ts index 0c25a69cd..ef750d1c8 100644 --- a/packages/vitnode/src/api/modules/admin/admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/admin.module.ts @@ -14,6 +14,7 @@ import { sendNotificationRoute } from "./routes/notifications.route"; import { sessionAdminRoute } from "./routes/session.route"; import { staffAdminModule } from "./staff/staff.admin.module"; import { usersAdminModule } from "./users/users.admin.module"; +import { warningsAdminModule } from "./warnings/warnings.admin.module"; export const adminModule = buildModule({ name: "admin", @@ -31,6 +32,7 @@ export const adminModule = buildModule({ notificationsAdminModule, pluginsAdminModule, reportReasonsAdminModule, + warningsAdminModule, ], cronJobs: [], }); diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts new file mode 100644 index 000000000..97acbfc00 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-reasons.routes.ts @@ -0,0 +1,326 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { count, eq, inArray, max, sql } from "drizzle-orm"; + +import { buildRoute } from "@/api/builders"; +import { multiLangAuditLabel } from "@/api/lib/audit/labels"; +import { writeAuditLog } from "@/api/lib/audit/write"; +import { + listWarningReasons, + WARNING_REASON_WORDS, + warningReasonSchema, +} from "@/api/lib/moderation/warnings/reasons"; +import { saveLanguageWords } from "@/api/lib/save-language-words"; +import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; +import { core_moderation_warning_reasons } from "@/database/warnings"; +import { auditDiff } from "@/lib/audit/diff"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; + +const wordsSchema = (maxLength: number) => + z.array( + z.object({ + languageCode: z.string(), + value: z.string().trim().max(maxLength), + }), + ); + +const reasonBody = z.object({ + title: z + .array( + z.object({ + languageCode: z.string(), + value: z + .string() + .trim() + .min(1) + .max(WARNING_LIMITS.reasonTitleMaxLength), + }), + ) + .min(1), + description: wordsSchema(WARNING_LIMITS.reasonDescriptionMaxLength), + message: wordsSchema(WARNING_LIMITS.reasonMessageMaxLength), + points: z.number().int().min(0).max(WARNING_LIMITS.maxPoints), + expiresAfterHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .nullable(), + enabled: z.boolean(), +}); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const LOCK_REASONS = sql`lock table ${core_moderation_warning_reasons} in share row exclusive mode`; + +const saveReasonWords = async ( + c: Context, + reasonId: number, + body: Partial>, +) => { + await Promise.all( + (["title", "description", "message"] as const).map(async variable => { + const values = body[variable]; + if (values === undefined) return; + await saveLanguageWords(c, { + ...WARNING_REASON_WORDS, + itemId: reasonId, + values: values.filter(item => item.value.length > 0), + variable, + }); + }), + ); +}; + +/** What an audit entry keeps of a reason: settings, never the words. */ +const auditedSettings = (body: Partial>) => ({ + enabled: body.enabled, + expiresAfterHours: body.expiresAfterHours, + points: body.points, +}); + +export const listWarningReasonsAdminRoute = buildRoute({ + adminStaffPermission: { module: "warning_settings", permission: "can_view" }, + route: { + method: "get", + path: "/reasons", + description: + "Every warning reason in display order, disabled ones too (Admin only)", + responses: { + 200: json( + z.object({ reasons: z.array(warningReasonSchema) }), + "Warning reasons", + ), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => c.json({ reasons: await listWarningReasons(c) }, 200), +}); + +export const createWarningReasonAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "post", + path: "/reasons", + description: `Add a warning reason at the end of the list (Admin only). There can be at most ${WARNING_LIMITS.maxReasons}`, + request: { + body: { + required: true, + content: { "application/json": { schema: reasonBody } }, + }, + }, + responses: { + 201: json(z.object({ id: z.number() }), "Warning reason created"), + 403: { description: "Access Denied" }, + 409: json(errorSchema, "The list already holds the maximum number"), + }, + }, + handler: async c => { + const body = c.req.valid("json"); + const created = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_REASONS); + const [{ position: lastPosition, total }] = await tx + .select({ + position: max(core_moderation_warning_reasons.position), + total: count(), + }) + .from(core_moderation_warning_reasons); + if (total >= WARNING_LIMITS.maxReasons) return null; + + const [row] = await tx + .insert(core_moderation_warning_reasons) + .values({ + enabled: body.enabled, + expiresAfterHours: body.expiresAfterHours, + points: body.points, + position: (lastPosition ?? -1) + 1, + }) + .returning({ id: core_moderation_warning_reasons.id }); + const label = multiLangAuditLabel(body.title); + await writeAuditLog( + c, + { + action: "warning_reason.created", + changes: auditDiff({ + after: { ...auditedSettings(body), title: label }, + before: null, + }), + target: { id: row.id, label, type: "warning_reason" }, + }, + tx, + ); + + return row; + }); + if (!created) return c.json({ error: "limit_reached" }, 409); + + await saveReasonWords(c, created.id, body); + await c + .get("events") + .emit("moderation.warning_reason.created", { reasonId: created.id }); + + return c.json({ id: created.id }, 201); + }, +}); + +export const updateWarningReasonAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "patch", + path: "/reasons/{id}", + description: + "Edit or disable a warning reason (Admin only). Warnings already issued keep the points, expiry and title they were issued with", + request: { + params: idParams, + body: { + required: true, + content: { "application/json": { schema: reasonBody.partial() } }, + }, + }, + responses: { + 200: json(z.object({ id: z.number() }), "Warning reason updated"), + 403: { description: "Access Denied" }, + 404: json(errorSchema, "Warning reason not found"), + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const body = c.req.valid("json"); + const settings = auditedSettings(body); + const patch = Object.fromEntries( + Object.entries(settings).filter(([, value]) => value !== undefined), + ); + + const found = await c.get("db").transaction(async tx => { + const [row] = await tx + .select({ + enabled: core_moderation_warning_reasons.enabled, + expiresAfterHours: core_moderation_warning_reasons.expiresAfterHours, + id: core_moderation_warning_reasons.id, + points: core_moderation_warning_reasons.points, + }) + .from(core_moderation_warning_reasons) + .where(eq(core_moderation_warning_reasons.id, id)) + .limit(1); + if (!row) return false; + if (Object.keys(patch).length > 0) { + await tx + .update(core_moderation_warning_reasons) + .set(patch) + .where(eq(core_moderation_warning_reasons.id, id)); + } + const [stored] = await listWarningReasons(c, { ids: [id] }); + const label = multiLangAuditLabel(body.title ?? stored?.title); + await writeAuditLog( + c, + { + action: "warning_reason.updated", + changes: auditDiff({ + after: { + ...row, + ...patch, + ...(body.title === undefined ? {} : { title: label }), + }, + before: row, + exclude: ["id"], + }), + metadata: { + ...(body.description === undefined + ? {} + : { descriptionChanged: true }), + ...(body.message === undefined ? {} : { messageChanged: true }), + }, + target: { id, label, type: "warning_reason" }, + }, + tx, + ); + + return true; + }); + if (!found) return c.json({ error: "not_found" }, 404); + + await saveReasonWords(c, id, body); + await c + .get("events") + .emit("moderation.warning_reason.updated", { reasonId: id }); + + return c.json({ id }, 200); + }, +}); + +export const reorderWarningReasonsAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "put", + path: "/reasons/reorder", + description: + "Set the order of every warning reason at once (Admin only). `ids` must list each reason exactly once", + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ ids: z.array(z.number().int()).min(1) }), + }, + }, + }, + }, + responses: { + 200: { description: "Order saved" }, + 400: json(errorSchema, "`ids` does not match the current reasons"), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => { + const { ids } = c.req.valid("json"); + const db = c.get("db"); + const current = await db + .select({ id: core_moderation_warning_reasons.id }) + .from(core_moderation_warning_reasons); + const known = new Set(current.map(row => row.id)); + if ( + ids.length !== known.size || + new Set(ids).size !== ids.length || + ids.some(id => !known.has(id)) + ) { + return c.json({ error: "mismatch" }, 400); + } + + const positionById = sql.join( + ids.map( + (id, position) => + sql`when ${core_moderation_warning_reasons.id} = ${id} then ${position}::integer`, + ), + sql` `, + ); + await db.transaction(async tx => { + await tx + .update(core_moderation_warning_reasons) + .set({ position: sql`case ${positionById} end` }) + .where(inArray(core_moderation_warning_reasons.id, ids)); + await writeAuditLog( + c, + { + action: "warning_reason.reordered", + metadata: { order: ids }, + target: { type: "warning_reason" }, + }, + tx, + ); + }); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts new file mode 100644 index 000000000..0558d7552 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/routes/warning-thresholds.routes.ts @@ -0,0 +1,301 @@ +import { z } from "@hono/zod-openapi"; +import { and, asc, count, eq, ne, sql } from "drizzle-orm"; + +import type { ContentDatabase } from "@/content/server/service"; + +import { buildRoute } from "@/api/builders"; +import { writeAuditLog } from "@/api/lib/audit/write"; +import { errorSchema, json } from "@/api/modules/moderation/lib/schema"; +import { core_moderation_warning_thresholds } from "@/database/warnings"; +import { auditDiff } from "@/lib/audit/diff"; +import { + WARNING_LIMITS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +const thresholdSchema = z.object({ + id: z.number(), + points: z.number(), + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + enabled: z.boolean(), + updatedAt: z.date(), +}); + +const thresholdFields = z.object({ + points: z.number().int().min(1).max(WARNING_LIMITS.maxThresholdPoints), + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .nullable(), + enabled: z.boolean(), +}); + +/** A temporary suspension needs its length; an indefinite one has none. */ +const thresholdBody = thresholdFields.refine( + value => + value.action === "suspend_temporary" + ? value.durationHours !== null + : value.durationHours === null, + { message: "duration_mismatch", path: ["durationHours"] }, +); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const LOCK_THRESHOLDS = sql`lock table ${core_moderation_warning_thresholds} in share row exclusive mode`; + +/** Whether another enabled rule already sits at `points`; rejected as ambiguous. */ +const pointsTaken = async ( + tx: ContentDatabase, + points: number, + exceptId?: number, +): Promise => { + const table = core_moderation_warning_thresholds; + const [row] = await tx + .select({ id: table.id }) + .from(table) + .where( + and( + eq(table.points, points), + eq(table.enabled, true), + exceptId === undefined ? undefined : ne(table.id, exceptId), + ), + ) + .limit(1); + + return row !== undefined; +}; + +const TARGET = "warning_threshold"; + +/** A threshold has no name; its points are what admins tell it apart by. */ +const thresholdLabel = ({ points }: { points: number }): string => + String(points); + +const responses = { + 400: json(errorSchema, "A temporary suspension needs a duration"), + 403: { description: "Access Denied" }, + 409: json( + errorSchema, + "Another enabled threshold uses the same points, or the limit is reached", + ), +}; + +export const listWarningThresholdsAdminRoute = buildRoute({ + adminStaffPermission: { module: "warning_settings", permission: "can_view" }, + route: { + method: "get", + path: "/thresholds", + description: "Every warning threshold, lowest points first (Admin only)", + responses: { + 200: json( + z.object({ thresholds: z.array(thresholdSchema) }), + "Warning thresholds", + ), + 403: { description: "Access Denied" }, + }, + }, + handler: async c => { + const table = core_moderation_warning_thresholds; + const thresholds = await c + .get("db") + .select({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + id: table.id, + points: table.points, + updatedAt: table.updatedAt, + }) + .from(table) + .orderBy(asc(table.points), asc(table.id)); + + return c.json({ thresholds }, 200); + }, +}); + +export const createWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "post", + path: "/thresholds", + description: + "Add a threshold (Admin only). It applies to warnings issued from now on; members already above it are not suspended", + request: { + body: { + required: true, + content: { "application/json": { schema: thresholdBody } }, + }, + }, + responses: { + 201: json(z.object({ id: z.number() }), "Threshold created"), + ...responses, + }, + }, + handler: async c => { + const body = c.req.valid("json"); + const table = core_moderation_warning_thresholds; + const result = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_THRESHOLDS); + const [{ total }] = await tx.select({ total: count() }).from(table); + if (total >= WARNING_LIMITS.maxThresholds) { + return { error: "limit_reached" } as const; + } + if (body.enabled && (await pointsTaken(tx, body.points))) { + return { error: "duplicate_points" } as const; + } + + const [row] = await tx + .insert(table) + .values(body) + .returning({ id: table.id }); + await writeAuditLog( + c, + { + action: "warning_threshold.created", + changes: auditDiff({ after: body, before: null }), + target: { id: row.id, label: thresholdLabel(body), type: TARGET }, + }, + tx, + ); + + return { id: row.id }; + }); + if ("error" in result) return c.json({ error: result.error }, 409); + + await c + .get("events") + .emit("moderation.warning_threshold.created", { thresholdId: result.id }); + + return c.json({ id: result.id }, 201); + }, +}); + +export const updateWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "put", + path: "/thresholds/{id}", + description: + "Edit, enable or disable a threshold (Admin only). Changes apply to future warnings only - nobody is suspended or unsuspended because of them", + request: { + params: idParams, + body: { + required: true, + content: { "application/json": { schema: thresholdBody } }, + }, + }, + responses: { + 200: json(z.object({ id: z.number() }), "Threshold updated"), + 404: json(errorSchema, "Threshold not found"), + ...responses, + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const body = c.req.valid("json"); + const table = core_moderation_warning_thresholds; + const result = await c.get("db").transaction(async tx => { + await tx.execute(LOCK_THRESHOLDS); + const [existing] = await tx + .select({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + points: table.points, + }) + .from(table) + .where(eq(table.id, id)) + .limit(1); + if (!existing) return { error: "not_found" } as const; + if (body.enabled && (await pointsTaken(tx, body.points, id))) { + return { error: "duplicate_points" } as const; + } + + await tx.update(table).set(body).where(eq(table.id, id)); + await writeAuditLog( + c, + { + action: "warning_threshold.updated", + changes: auditDiff({ after: body, before: existing }), + target: { id, label: thresholdLabel(body), type: TARGET }, + }, + tx, + ); + + return { id }; + }); + if ("error" in result) { + return result.error === "not_found" + ? c.json({ error: result.error }, 404) + : c.json({ error: result.error }, 409); + } + + await c + .get("events") + .emit("moderation.warning_threshold.updated", { thresholdId: id }); + + return c.json({ id }, 200); + }, +}); + +export const deleteWarningThresholdAdminRoute = buildRoute({ + adminStaffPermission: { + module: "warning_settings", + permission: "can_manage", + }, + route: { + method: "delete", + path: "/thresholds/{id}", + description: + "Delete a threshold (Admin only). Suspensions it created stay and keep a copy of the rule", + request: { params: idParams }, + responses: { + 200: { description: "Threshold deleted" }, + 403: { description: "Access Denied" }, + 404: json(errorSchema, "Threshold not found"), + }, + }, + handler: async c => { + const { id } = c.req.valid("param"); + const table = core_moderation_warning_thresholds; + const deleted = await c.get("db").transaction(async tx => { + const [row] = await tx.delete(table).where(eq(table.id, id)).returning({ + action: table.action, + durationHours: table.durationHours, + enabled: table.enabled, + points: table.points, + }); + if (!row) return false; + await writeAuditLog( + c, + { + action: "warning_threshold.deleted", + changes: auditDiff({ after: null, before: row }), + target: { id, label: thresholdLabel(row), type: TARGET }, + }, + tx, + ); + + return true; + }); + if (!deleted) return c.json({ error: "not_found" }, 404); + + await c + .get("events") + .emit("moderation.warning_threshold.deleted", { thresholdId: id }); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts b/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts new file mode 100644 index 000000000..70a5b012a --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/warnings/warnings.admin.module.ts @@ -0,0 +1,28 @@ +import { buildModule } from "@/api/builders"; + +import { + createWarningReasonAdminRoute, + listWarningReasonsAdminRoute, + reorderWarningReasonsAdminRoute, + updateWarningReasonAdminRoute, +} from "./routes/warning-reasons.routes"; +import { + createWarningThresholdAdminRoute, + deleteWarningThresholdAdminRoute, + listWarningThresholdsAdminRoute, + updateWarningThresholdAdminRoute, +} from "./routes/warning-thresholds.routes"; + +export const warningsAdminModule = buildModule({ + name: "warnings", + routes: [ + listWarningReasonsAdminRoute, + createWarningReasonAdminRoute, + reorderWarningReasonsAdminRoute, + updateWarningReasonAdminRoute, + listWarningThresholdsAdminRoute, + createWarningThresholdAdminRoute, + updateWarningThresholdAdminRoute, + deleteWarningThresholdAdminRoute, + ], +}); diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts new file mode 100644 index 000000000..b448da35e --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-service.ts @@ -0,0 +1,914 @@ +import type { Context } from "hono"; + +import { and, eq, isNull, sql } from "drizzle-orm"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; +import type { RegisteredReportTarget } from "@/api/lib/moderation/targets"; +import type { WarningReason } from "@/api/lib/moderation/warnings/reasons"; +import type { ContentDatabase } from "@/content/server/service"; +import type { + WarningContentSnapshot, + WarningThresholdAction, +} from "@/lib/moderation/warnings"; + +import { userAuditLabel } from "@/api/lib/audit/labels"; +import { isUniqueViolation } from "@/api/lib/db-errors"; +import { canOnReport } from "@/api/lib/moderation/access"; +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { + canSeeWarning, + canWarnForContent, + hasWarningPermission, + warningTargetRefusal, +} from "@/api/lib/moderation/warnings/access"; +import { recordWarningAudit } from "@/api/lib/moderation/warnings/audit"; +import { listWarningReasons } from "@/api/lib/moderation/warnings/reasons"; +import { forgetSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + accountSuspendedNotification, + suspensionRevokedNotification, + warningIssuedNotification, + warningRevokedNotification, +} from "@/api/lib/notifications/core-types"; +import { core_users } from "@/database/users"; +import { + core_moderation_sanctions, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { MODERATION_PERMISSIONS } from "@/lib/moderation/const"; +import { + activeWarningPoints, + evaluateWarningThresholds, + thresholdSuspensionEnd, + WARNING_LIMITS, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; + +import type { ReportRow } from "./reports"; + +import { + appendReportEvent, + authorizeReport, + reportConflict, + reportError, + updateReport, +} from "./reports"; + +const HOUR_MS = 60 * 60 * 1000; + +export const warningError = reportError; + +/** How the moderator wants the points to expire. */ +export type WarningExpirationInput = + | { at: Date; mode: "at" } + | { hours: number; mode: "hours" } + | { mode: "default" } + | { mode: "never" }; + +export interface WarningContentInput { + pluginId: string; + targetId: string; + targetType: string; +} + +/** What preview and issue share: who, why, how many points, and what for. */ +export interface WarningDraftInput { + content?: null | WarningContentInput; + expiration: WarningExpirationInput; + points?: null | number; + reasonId: null | number; + reportId?: null | number; + userId: number; +} + +export interface IssueWarningInput extends WarningDraftInput { + idempotencyKey: string; + message: string; + overrideNote?: null | string; + resolveReport?: boolean; + staffNote?: null | string; +} + +export interface WarningTargetUser { + id: number; + name: string; + roleId: number; +} + +export interface ResolvedWarningContent { + pluginId: string; + scopeFamily: null | string; + scopeId: null | string; + snapshot: WarningContentSnapshot; + target: RegisteredReportTarget; + targetId: string; + targetType: string; +} + +/** A draft after every server-side check: what the warning would be. */ +export interface ResolvedWarningDraft { + content: null | ResolvedWarningContent; + /** The reason's expiration in hours, `null` for never. */ + defaultExpiresAfterHours: null | number; + defaultPoints: number; + /** Computes the expiry against the time the warning is written. */ + expiresAt: (now: Date) => Date | null; + overrides: { expiration: boolean; points: boolean }; + points: number; + reason: null | WarningReason; + report: null | ReportRow; + user: WarningTargetUser; +} + +const loadTargetUser = async ( + c: Context, + userId: number, +): Promise => { + const [user] = await c + .get("db") + .select({ + id: core_users.id, + name: core_users.name, + roleId: core_users.roleId, + }) + .from(core_users) + .where(eq(core_users.id, userId)) + .limit(1); + + return user ?? null; +}; + +const resolveContent = async ( + c: Context, + moderator: ModeratorContext, + content: WarningContentInput, + userId: number, +): Promise => { + const target = findReportTarget(c.get("core").moderation, { + pluginId: content.pluginId, + type: content.targetType, + }); + const resolveAuthors = target?.definition.resolveAuthors; + if (!target || !resolveAuthors) { + throw warningError(404, { error: "content_not_found" }); + } + const targetId = target.definition.parseId(content.targetId); + if (targetId === null) + throw warningError(404, { error: "content_not_found" }); + + const { definition } = target; + let scopeId: null | string = null; + if (definition.scope && definition.resolveScope) { + const resolved = await definition.resolveScope(c, targetId); + if (resolved === undefined) { + throw warningError(404, { error: "content_not_found" }); + } + scopeId = resolved === "" ? null : resolved; + } + + const ref = { + pluginId: target.pluginId, + scopeId, + targetType: definition.type, + }; + if (!canWarnForContent(c, moderator.set, ref)) { + throw warningError(403, { error: "forbidden" }); + } + + const authors = await resolveAuthors(c, { targetId }); + if (!authors) throw warningError(404, { error: "content_not_found" }); + if (!authors.authorIds.includes(userId)) { + throw warningError(400, { error: "author_mismatch" }); + } + const current = await definition.current(c, { locale: null, targetId }); + + return { + pluginId: target.pluginId, + scopeFamily: definition.scope ?? null, + scopeId, + snapshot: { + title: authors.title ? authors.title.slice(0, 500) : null, + url: current?.url ?? null, + }, + target, + targetId, + targetType: definition.type, + }; +}; + +const sameContent = ( + a: WarningContentInput, + b: Pick, +): boolean => + a.pluginId === b.pluginId && + a.targetType === b.targetType && + a.targetId === b.targetId; + +/** + * Runs every check a warning needs - target protection, reason, scope, + * authorship, report match, override permissions - and resolves what the + * warning would hold. Never trusts a client-sent author or scope: both come + * from the content's own report target. + */ +export const resolveWarningDraft = async ( + c: Context, + moderator: ModeratorContext, + input: WarningDraftInput, +): Promise => { + const user = await loadTargetUser(c, input.userId); + if (!user) throw warningError(404, { error: "user_not_found" }); + + const refusal = await warningTargetRefusal(c, moderator, user); + if (refusal) throw warningError(403, { error: refusal }); + + let report: null | ReportRow = null; + let contentInput = input.content ?? null; + if (input.reportId) { + ({ report } = await authorizeReport(c, moderator, input.reportId)); + if (contentInput && !sameContent(contentInput, report)) { + throw warningError(400, { error: "report_mismatch" }); + } + contentInput = { + pluginId: report.pluginId, + targetId: report.targetId, + targetType: report.targetType, + }; + } + + const content = contentInput + ? await resolveContent(c, moderator, contentInput, user.id) + : null; + if ( + !content && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.warn) + ) { + throw warningError(403, { error: "forbidden" }); + } + + let reason: null | WarningReason = null; + if (input.reasonId !== null) { + [reason = null] = await listWarningReasons(c, { ids: [input.reasonId] }); + if (!reason) throw warningError(404, { error: "reason_not_found" }); + if (!reason.enabled) throw warningError(409, { error: "reason_disabled" }); + } + + const defaultPoints = reason?.points ?? 0; + const defaultExpiresAfterHours = reason?.expiresAfterHours ?? null; + const points = input.points ?? defaultPoints; + if ( + !Number.isInteger(points) || + points < 0 || + points > WARNING_LIMITS.maxPoints + ) { + throw warningError(400, { error: "invalid_points" }); + } + + const { expiration } = input; + const overrides = { + expiration: + expiration.mode === "at" || + (expiration.mode === "never" && defaultExpiresAfterHours !== null) || + (expiration.mode === "hours" && + expiration.hours !== defaultExpiresAfterHours), + points: points !== defaultPoints, + }; + if ( + overrides.points && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.overridePoints) + ) { + throw warningError(403, { error: "points_override_forbidden" }); + } + if ( + overrides.expiration && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.overrideExpiration) + ) { + throw warningError(403, { error: "expiration_override_forbidden" }); + } + + const requestTime = Date.now(); + if (expiration.mode === "hours") { + if ( + !Number.isInteger(expiration.hours) || + expiration.hours < 1 || + expiration.hours > WARNING_LIMITS.maxDurationHours + ) { + throw warningError(400, { error: "invalid_expiration" }); + } + } + if (expiration.mode === "at") { + const at = expiration.at.getTime(); + if ( + Number.isNaN(at) || + at <= requestTime || + at > requestTime + WARNING_LIMITS.maxDurationHours * HOUR_MS + ) { + throw warningError(400, { error: "invalid_expiration" }); + } + } + + const expiresAt = (now: Date): Date | null => { + switch (expiration.mode) { + case "at": + return expiration.at; + case "default": + return defaultExpiresAfterHours === null + ? null + : new Date(now.getTime() + defaultExpiresAfterHours * HOUR_MS); + case "hours": + return new Date(now.getTime() + expiration.hours * HOUR_MS); + case "never": + return null; + } + }; + + return { + content, + defaultExpiresAfterHours, + defaultPoints, + expiresAt, + overrides, + points, + reason, + report, + user, + }; +}; + +type Db = ContentDatabase; + +const lockUserWarnings = async (tx: Db, userId: number): Promise => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`core_moderation_warnings:${userId}`}))`, + ); +}; + +/** The user's active points at `now`, read through `db`. */ +export const loadActivePoints = async ( + db: Db, + userId: number, + now: Date, +): Promise => { + const rows = await db + .select({ + expiresAt: core_moderation_warnings.expiresAt, + points: core_moderation_warnings.points, + revokedAt: core_moderation_warnings.revokedAt, + }) + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)); + + return activeWarningPoints(rows, now); +}; + +export const loadEnabledThresholds = async (db: Db) => + await db + .select({ + action: core_moderation_warning_thresholds.action, + durationHours: core_moderation_warning_thresholds.durationHours, + enabled: core_moderation_warning_thresholds.enabled, + id: core_moderation_warning_thresholds.id, + points: core_moderation_warning_thresholds.points, + }) + .from(core_moderation_warning_thresholds) + .where(eq(core_moderation_warning_thresholds.enabled, true)); + +export interface ProjectedSanction { + action: WarningThresholdAction; + durationHours: null | number; + endsAt: Date | null; + points: number; + thresholdId: number; +} + +/** + * What a warning of `points` expiring at `expiresAt` would do to the user now. + * Advisory: {@link issueWarning} computes it again under the user's lock. + */ +export const projectWarning = async ( + c: Context, + { + expiresAt, + points, + userId, + }: { expiresAt: Date | null; points: number; userId: number }, +) => { + const db = c.get("db"); + const now = new Date(); + const [before, rules] = await Promise.all([ + loadActivePoints(db, userId, now), + loadEnabledThresholds(db), + ]); + const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); + const after = before + (counts ? points : 0); + const { applied, crossed } = evaluateWarningThresholds({ + after, + before, + rules, + }); + + return { + after, + before, + crossed: crossed.map(rule => rule.id), + sanction: applied + ? { + action: applied.action, + durationHours: applied.durationHours, + endsAt: thresholdSuspensionEnd(applied, now), + points: applied.points, + thresholdId: applied.id, + } + : null, + } satisfies { + after: number; + before: number; + crossed: number[]; + sanction: null | ProjectedSanction; + }; +}; + +export interface IssuedWarning { + duplicate: boolean; + points: { after: number; before: number }; + reportResolved: boolean; + sanction: null | { + endsAt: Date | null; + id: number; + thresholdId: null | number; + }; + warningId: number; +} + +const findSanctionOfWarning = async (db: Db, warningId: number) => { + const [sanction] = await db + .select({ + endsAt: core_moderation_sanctions.endsAt, + id: core_moderation_sanctions.id, + thresholdId: core_moderation_sanctions.thresholdId, + }) + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.warningId, warningId)) + .limit(1); + + return sanction ?? null; +}; + +const findIssuedWarning = async ( + db: Db, + moderator: ModeratorContext, + { + idempotencyKey, + userId, + }: Pick, +): Promise => { + const [existing] = await db + .select({ + id: core_moderation_warnings.id, + pointsAfter: core_moderation_warnings.pointsAfter, + pointsBefore: core_moderation_warnings.pointsBefore, + reportId: core_moderation_warnings.reportId, + userId: core_moderation_warnings.userId, + }) + .from(core_moderation_warnings) + .where( + and( + eq(core_moderation_warnings.issuedById, moderator.user.id), + eq(core_moderation_warnings.idempotencyKey, idempotencyKey), + ), + ) + .limit(1); + if (!existing) return null; + if (existing.userId !== userId) { + throw warningError(409, { error: "idempotency_conflict" }); + } + + return { + duplicate: true, + points: { after: existing.pointsAfter, before: existing.pointsBefore }, + reportResolved: false, + sanction: await findSanctionOfWarning(db, existing.id), + warningId: existing.id, + }; +}; + +/** + * Issues a warning. Serialized per warned user with a transaction-scoped + * advisory lock, so concurrent warnings read each other's points and a + * threshold creates at most one sanction. The warning, the threshold decision, + * the sanction, the audit entries, the optional report resolution and the + * notifications commit together or not at all. A retry with the same + * `idempotencyKey` from the same moderator returns the first result. + */ +export const issueWarning = async ( + c: Context, + moderator: ModeratorContext, + input: IssueWarningInput, +): Promise => { + // A retry of a request that already went through answers with its result, + // even when the report it resolved is closed or the content is gone now. + const previous = await findIssuedWarning(c.get("db"), moderator, input); + if (previous) return previous; + + const draft = await resolveWarningDraft(c, moderator, input); + const overridden = draft.overrides.points || draft.overrides.expiration; + const overrideNote = input.overrideNote?.trim() ?? ""; + const staffNote = input.staffNote?.trim() ?? ""; + if (overridden && overrideNote.length < WARNING_LIMITS.explanationMinLength) { + throw warningError(400, { error: "override_note_required" }); + } + + const { report } = draft; + const resolveReport = Boolean(report && input.resolveReport); + if (report && resolveReport) { + const manage = canOnReport( + c, + moderator.set, + { + pluginId: report.pluginId, + scopeId: report.scopeId, + targetType: report.targetType, + }, + MODERATION_PERMISSIONS.manageReports, + ); + if (!manage) throw warningError(403, { error: "forbidden" }); + if (report.status === "done") throw reportConflict(report, "report_closed"); + } + + const issue = async () => + await c.get("db").transaction(async (tx: Db) => { + await lockUserWarnings(tx, draft.user.id); + + const existing = await findIssuedWarning(tx, moderator, input); + if (existing) return existing; + + const now = new Date(); + const expiresAt = draft.expiresAt(now); + const before = await loadActivePoints(tx, draft.user.id, now); + const counts = expiresAt === null || expiresAt.getTime() > now.getTime(); + const after = before + (counts ? draft.points : 0); + + const [warning] = await tx + .insert(core_moderation_warnings) + .values({ + contentSnapshot: draft.content?.snapshot ?? null, + defaultPoints: draft.defaultPoints, + expiresAt, + idempotencyKey: input.idempotencyKey, + issuedAt: now, + issuedById: moderator.user.id, + message: input.message.trim(), + overrideNote: overridden ? overrideNote : null, + pluginId: draft.content?.pluginId ?? null, + points: draft.points, + pointsAfter: after, + pointsBefore: before, + reasonId: draft.reason?.id ?? null, + reasonSnapshot: draft.reason ? { title: draft.reason.title } : null, + reportId: report?.id ?? null, + scopeFamily: draft.content?.scopeFamily ?? null, + scopeId: draft.content?.scopeId ?? null, + staffNote: staffNote === "" ? null : staffNote, + targetId: draft.content?.targetId ?? null, + targetType: draft.content?.targetType ?? null, + userId: draft.user.id, + }) + .returning({ id: core_moderation_warnings.id }); + + const rules = await loadEnabledThresholds(tx); + const { applied, crossed } = evaluateWarningThresholds({ + after, + before, + rules, + }); + const scope = + draft.content?.scopeFamily && draft.content.scopeId + ? { family: draft.content.scopeFamily, id: draft.content.scopeId } + : null; + await recordWarningAudit(c, tx, { + action: "warning.issued", + metadata: { + appliedThresholdId: applied?.id ?? null, + contentId: draft.content?.targetId ?? null, + contentPluginId: draft.content?.pluginId ?? null, + contentType: draft.content?.targetType ?? null, + crossedThresholdIds: crossed.map(rule => rule.id), + defaultExpiresAfterHours: draft.defaultExpiresAfterHours, + defaultPoints: draft.defaultPoints, + expirationOverridden: draft.overrides.expiration, + expiresAt: expiresAt?.toISOString() ?? null, + points: draft.points, + pointsAfter: after, + pointsBefore: before, + pointsOverridden: draft.overrides.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, + warningId: warning.id, + }, + scope, + user: draft.user, + }); + + let sanction: IssuedWarning["sanction"] = null; + if (applied) { + const endsAt = thresholdSuspensionEnd(applied, now); + const [row] = await tx + .insert(core_moderation_sanctions) + .values({ + createdById: moderator.user.id, + endsAt, + source: "threshold", + startsAt: now, + thresholdId: applied.id, + thresholdSnapshot: { + action: applied.action, + durationHours: applied.durationHours, + points: applied.points, + }, + type: "suspension", + userId: draft.user.id, + warningId: warning.id, + }) + .returning({ id: core_moderation_sanctions.id }); + sanction = { endsAt, id: row.id, thresholdId: applied.id }; + await recordWarningAudit(c, tx, { + action: "sanction.applied", + metadata: { + endsAt: endsAt?.toISOString() ?? null, + sanctionId: row.id, + source: "threshold", + thresholdAction: applied.action, + thresholdId: applied.id, + thresholdPoints: applied.points, + warningId: warning.id, + }, + scope, + user: draft.user, + }); + } + + if (report) { + await appendReportEvent(tx, { + actorId: moderator.user.id, + data: { + points: draft.points, + userId: draft.user.id, + warningId: warning.id, + }, + reportId: report.id, + type: "warning", + }); + if (resolveReport) { + await updateReport(tx, report, { + openKey: null, + resolution: "action_taken", + resolvedAt: now, + resolvedById: moderator.user.id, + status: "done", + }); + await appendReportEvent(tx, { + actorId: moderator.user.id, + data: { resolution: "action_taken", warningId: warning.id }, + reportId: report.id, + type: "resolved", + }); + } + } + + const notifications = c.get("notifications"); + await notifications.publish({ + actorId: null, + data: { + message: input.message.trim(), + points: draft.points, + reason: draft.reason?.title ?? null, + warningId: warning.id, + }, + idempotencyKey: `warning:${warning.id}`, + recipients: [draft.user.id], + tx, + type: warningIssuedNotification, + }); + if (sanction) { + await notifications.publish({ + actorId: null, + data: { + endsAt: sanction.endsAt?.toISOString() ?? null, + sanctionId: sanction.id, + }, + idempotencyKey: `sanction:${sanction.id}`, + recipients: [draft.user.id], + tx, + type: accountSuspendedNotification, + }); + } + + return { + duplicate: false, + points: { after, before }, + reportResolved: resolveReport, + sanction, + warningId: warning.id, + } satisfies IssuedWarning; + }); + + // The lock is per warned member, so the same key sent at once for two + // members only meets at the unique (issuedById, idempotencyKey) index. + const result = await issue().catch(async (error: unknown) => { + if (!isUniqueViolation(error)) throw error; + const existing = await findIssuedWarning(c.get("db"), moderator, input); + if (!existing) throw error; + + return existing; + }); + + if (result.duplicate) return result; + + if (result.sanction) { + await forgetSuspension(c, draft.user.id, { disconnect: true }); + } + const events = c.get("events"); + await events.emit("moderation.warning.issued", { + issuedById: moderator.user.id, + pluginId: draft.content?.pluginId ?? null, + points: draft.points, + reasonId: draft.reason?.id ?? null, + reportId: report?.id ?? null, + targetId: draft.content?.targetId ?? null, + targetType: draft.content?.targetType ?? null, + userId: draft.user.id, + warningId: result.warningId, + }); + if (result.sanction) { + await events.emit("moderation.sanction.applied", { + endsAt: result.sanction.endsAt, + sanctionId: result.sanction.id, + source: "threshold", + thresholdId: result.sanction.thresholdId, + type: "suspension", + userId: draft.user.id, + warningId: result.warningId, + }); + } + if (report && result.reportResolved) { + await events.emit("moderation.report.resolved", { + reportId: report.id, + resolution: "action_taken", + }); + } + + return result; +}; + +/** + * Revokes a warning: its points stop counting at once, the record stays. + * Sanctions it led to stay too - lifting them is a separate action. + */ +export const revokeWarning = async ( + c: Context, + moderator: ModeratorContext, + { id, reason }: { id: number; reason: string }, +) => { + const db = c.get("db"); + const [warning] = await db + .select() + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.id, id)) + .limit(1); + if (!warning || !canSeeWarning(c, moderator, warning)) { + throw warningError(404, { error: "not_found" }); + } + if (!hasWarningPermission(moderator.set, WARNING_PERMISSIONS.revoke)) { + throw warningError(403, { error: "forbidden" }); + } + if (warning.revokedAt) throw warningError(409, { error: "already_revoked" }); + + const revoked = await db.transaction(async (tx: Db) => { + const now = new Date(); + const [row] = await tx + .update(core_moderation_warnings) + .set({ + revokedAt: now, + revokedById: moderator.user.id, + revokeReason: reason.trim(), + }) + .where( + and( + eq(core_moderation_warnings.id, id), + isNull(core_moderation_warnings.revokedAt), + ), + ) + .returning({ id: core_moderation_warnings.id }); + if (!row) return false; + + await recordWarningAudit(c, tx, { + action: "warning.revoked", + metadata: { points: warning.points, warningId: id }, + scope: + warning.scopeFamily && warning.scopeId + ? { family: warning.scopeFamily, id: warning.scopeId } + : null, + user: { + id: warning.userId, + name: await userAuditLabel(c, warning.userId), + }, + }); + await c.get("notifications").publish({ + actorId: null, + data: { + points: warning.points, + reason: warning.reasonSnapshot?.title ?? null, + warningId: id, + }, + idempotencyKey: `warning-revoked:${id}`, + recipients: [warning.userId], + tx, + type: warningRevokedNotification, + }); + + return true; + }); + if (!revoked) throw warningError(409, { error: "already_revoked" }); + + await c.get("events").emit("moderation.warning.revoked", { + revokedById: moderator.user.id, + userId: warning.userId, + warningId: id, + }); + + return { userId: warning.userId, warningId: id }; +}; + +/** + * Lifts one sanction. Other active sanctions of the same user keep applying. + */ +export const revokeSanction = async ( + c: Context, + moderator: ModeratorContext, + { id, reason }: { id: number; reason: string }, +) => { + if ( + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.manageSanctions) + ) { + throw warningError(403, { error: "forbidden" }); + } + const db = c.get("db"); + const [sanction] = await db + .select() + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.id, id)) + .limit(1); + if (!sanction) throw warningError(404, { error: "not_found" }); + if (sanction.revokedAt) throw warningError(409, { error: "already_revoked" }); + + const revoked = await db.transaction(async (tx: Db) => { + const [row] = await tx + .update(core_moderation_sanctions) + .set({ + revokedAt: new Date(), + revokedById: moderator.user.id, + revokeReason: reason.trim(), + }) + .where( + and( + eq(core_moderation_sanctions.id, id), + isNull(core_moderation_sanctions.revokedAt), + ), + ) + .returning({ id: core_moderation_sanctions.id }); + if (!row) return false; + + await recordWarningAudit(c, tx, { + action: "sanction.revoked", + metadata: { + sanctionId: id, + type: sanction.type, + warningId: sanction.warningId, + }, + user: { + id: sanction.userId, + name: await userAuditLabel(c, sanction.userId), + }, + }); + await c.get("notifications").publish({ + actorId: null, + data: { sanctionId: id }, + idempotencyKey: `sanction-revoked:${id}`, + recipients: [sanction.userId], + tx, + type: suspensionRevokedNotification, + }); + + return true; + }); + if (!revoked) throw warningError(409, { error: "already_revoked" }); + + await forgetSuspension(c, sanction.userId, { disconnect: false }); + await c.get("events").emit("moderation.sanction.revoked", { + revokedById: moderator.user.id, + sanctionId: id, + userId: sanction.userId, + }); + + return { sanctionId: id, userId: sanction.userId }; +}; diff --git a/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts b/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts new file mode 100644 index 000000000..98b5ef64d --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/lib/warnings-view.ts @@ -0,0 +1,223 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { desc, eq, inArray } from "drizzle-orm"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; + +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { + canSeeWarning, + canSeeWarningNotes, + hasWarningPermission, +} from "@/api/lib/moderation/warnings/access"; +import { + core_moderation_sanctions, + core_moderation_warnings, +} from "@/database/warnings"; +import { + isSanctionActiveAt, + SANCTION_SOURCES, + SANCTION_TYPES, + WARNING_PERMISSIONS, + WARNING_STATUSES, + WARNING_THRESHOLD_ACTIONS, + warningStatusAt, +} from "@/lib/moderation/warnings"; + +import { loadReportPeople } from "./people"; +import { reportPersonSchema } from "./schema"; + +const textSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +export const warningContentSchema = z.object({ + pluginId: z.string(), + targetType: z.string(), + targetId: z.string(), + targetLabel: z.string().nullable(), + title: z.string().nullable(), + url: z.string().nullable(), +}); + +/** A warning as staff see it; notes are `null` without `can_view_notes`. */ +export const staffWarningSchema = z.object({ + id: z.number(), + userId: z.number(), + status: z.enum(WARNING_STATUSES), + points: z.number(), + defaultPoints: z.number().nullable(), + reasonId: z.number().nullable(), + reason: textSchema.nullable(), + message: z.string(), + staffNote: z.string().nullable(), + overrideNote: z.string().nullable(), + issuedAt: z.date(), + expiresAt: z.date().nullable(), + issuedBy: reportPersonSchema.nullable(), + content: warningContentSchema.nullable(), + reportId: z.number().nullable(), + revokedAt: z.date().nullable(), + revokedBy: reportPersonSchema.nullable(), + revokeReason: z.string().nullable(), + sanctionId: z.number().nullable(), +}); + +export const sanctionSchema = z.object({ + id: z.number(), + type: z.enum(SANCTION_TYPES), + source: z.enum(SANCTION_SOURCES), + active: z.boolean(), + startsAt: z.date(), + endsAt: z.date().nullable(), + warningId: z.number().nullable(), + threshold: z + .object({ + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + points: z.number(), + }) + .nullable(), + createdBy: reportPersonSchema.nullable(), + revokedAt: z.date().nullable(), + revokedBy: reportPersonSchema.nullable(), + revokeReason: z.string().nullable(), +}); + +export type WarningRow = typeof core_moderation_warnings.$inferSelect; +export type SanctionRow = typeof core_moderation_sanctions.$inferSelect; + +const contentOf = (c: Context, row: WarningRow) => { + if (!row.pluginId || !row.targetType || !row.targetId) return null; + + return { + pluginId: row.pluginId, + targetType: row.targetType, + targetId: row.targetId, + targetLabel: + findReportTarget(c.get("core").moderation, { + pluginId: row.pluginId, + type: row.targetType, + })?.definition.label ?? null, + title: row.contentSnapshot?.title ?? null, + url: row.contentSnapshot?.url ?? null, + }; +}; + +/** + * Turns warning rows into what one moderator may read: rows they may not + * see are dropped, and staff notes are blanked unless allowed. + */ +export const serializeStaffWarnings = async ( + c: Context, + moderator: ModeratorContext, + rows: WarningRow[], + now: Date = new Date(), +) => { + const visible = rows.filter(row => canSeeWarning(c, moderator, row)); + const canViewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + const [people, sanctions] = await Promise.all([ + loadReportPeople( + c, + visible.flatMap(row => [row.issuedById, row.revokedById]), + ), + canViewSanctions && visible.length > 0 + ? c + .get("db") + .select({ + id: core_moderation_sanctions.id, + warningId: core_moderation_sanctions.warningId, + }) + .from(core_moderation_sanctions) + .where( + inArray( + core_moderation_sanctions.warningId, + visible.map(row => row.id), + ), + ) + : [], + ]); + const person = (id: null | number) => + id === null ? null : (people.get(id) ?? null); + + return visible.map(row => { + const notes = canSeeWarningNotes(moderator, row); + + return { + id: row.id, + userId: row.userId, + status: warningStatusAt(row, now), + points: row.points, + defaultPoints: row.defaultPoints, + reasonId: row.reasonId, + reason: row.reasonSnapshot?.title ?? null, + message: row.message, + staffNote: notes ? row.staffNote : null, + overrideNote: notes ? row.overrideNote : null, + issuedAt: row.issuedAt, + expiresAt: row.expiresAt, + issuedBy: person(row.issuedById), + content: contentOf(c, row), + reportId: row.reportId, + revokedAt: row.revokedAt, + revokedBy: person(row.revokedById), + revokeReason: row.revokeReason, + sanctionId: + sanctions.find(sanction => sanction.warningId === row.id)?.id ?? null, + }; + }); +}; + +export const serializeSanctions = async ( + c: Context, + rows: SanctionRow[], + now: Date = new Date(), +) => { + const people = await loadReportPeople( + c, + rows.flatMap(row => [row.createdById, row.revokedById]), + ); + const person = (id: null | number) => + id === null ? null : (people.get(id) ?? null); + + return rows.map(row => ({ + id: row.id, + type: row.type, + source: row.source, + active: isSanctionActiveAt(row, now), + startsAt: row.startsAt, + endsAt: row.endsAt, + warningId: row.warningId, + threshold: row.thresholdSnapshot, + createdBy: person(row.createdById), + revokedAt: row.revokedAt, + revokedBy: person(row.revokedById), + revokeReason: row.revokeReason, + })); +}; + +export const loadUserWarnings = async (c: Context, userId: number) => + await c + .get("db") + .select() + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)) + .orderBy( + desc(core_moderation_warnings.issuedAt), + desc(core_moderation_warnings.id), + ); + +export const loadUserSanctions = async (c: Context, userId: number) => + await c + .get("db") + .select() + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.userId, userId)) + .orderBy( + desc(core_moderation_sanctions.startsAt), + desc(core_moderation_sanctions.id), + ); diff --git a/packages/vitnode/src/api/modules/moderation/moderation.module.ts b/packages/vitnode/src/api/modules/moderation/moderation.module.ts index 390c2a50a..f5517fb83 100644 --- a/packages/vitnode/src/api/modules/moderation/moderation.module.ts +++ b/packages/vitnode/src/api/modules/moderation/moderation.module.ts @@ -1,5 +1,6 @@ import { buildModule } from "@/api/builders"; +import { accountStatusRoute } from "./routes/account-status.route"; import { approvalFiltersRoute, approveApprovalRoute, @@ -16,6 +17,17 @@ import { } from "./routes/notes.routes"; import { listReportReasonsRoute } from "./routes/reasons.route"; import { showReportRoute } from "./routes/show.route"; +import { + issueWarningRoute, + listWarningReasonsRoute, + listWarningsRoute, + memberLogRoute, + memberStandingRoute, + previewWarningRoute, + revokeSanctionRoute, + revokeWarningRoute, + searchMembersRoute, +} from "./routes/warnings.routes"; import { assignReportRoute, claimReportRoute, @@ -48,5 +60,15 @@ export const moderationModule = buildModule({ showApprovalRoute, approveApprovalRoute, rejectApprovalRoute, + listWarningReasonsRoute, + previewWarningRoute, + issueWarningRoute, + listWarningsRoute, + revokeWarningRoute, + revokeSanctionRoute, + searchMembersRoute, + memberStandingRoute, + memberLogRoute, + accountStatusRoute, ], }); diff --git a/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts new file mode 100644 index 000000000..320219770 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/routes/account-status.route.ts @@ -0,0 +1,127 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/builders"; +import { findReportTarget } from "@/api/lib/moderation/registry"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + isSanctionActiveAt, + WARNING_STATUSES, + warningStatusAt, +} from "@/lib/moderation/warnings"; + +import { json } from "../lib/schema"; +import { loadActivePoints } from "../lib/warnings-service"; +import { loadUserSanctions, loadUserWarnings } from "../lib/warnings-view"; + +const textSchema = z.array( + z.object({ languageCode: z.string(), value: z.string() }), +); + +/** + * What a member may read about their own warnings. Built field by field so + * staff notes, override explanations, revocation reasons and the issuing + * moderator never leave the server. + */ +const ownWarningSchema = z.object({ + id: z.number(), + status: z.enum(WARNING_STATUSES), + points: z.number(), + reason: textSchema.nullable(), + message: z.string(), + issuedAt: z.date(), + expiresAt: z.date().nullable(), + content: z + .object({ title: z.string().nullable(), url: z.string().nullable() }) + .nullable(), +}); + +const ownSanctionSchema = z.object({ + id: z.number(), + active: z.boolean(), + startsAt: z.date(), + endsAt: z.date().nullable(), + revoked: z.boolean(), + warningId: z.number().nullable(), +}); + +export const accountStatusRoute = buildRoute({ + route: { + method: "get", + path: "/account-status", + description: + "The signed-in member's own standing: active warning points, warning history and suspensions. Also answers for a suspended account, whose session is otherwise treated as a guest. Never includes staff notes", + responses: { + 200: json( + z.object({ + suspension: z.object({ endsAt: z.date().nullable() }).nullable(), + activePoints: z.number(), + warnings: z.array(ownWarningSchema), + sanctions: z.array(ownSanctionSchema), + }), + "The account's standing", + ), + 401: { description: "Sign in first" }, + }, + }, + handler: async c => { + const userId = c.get("user")?.id ?? c.get("suspension")?.userId; + if (!userId) return c.json({ error: "unauthenticated" }, 401); + + const now = new Date(); + const [warnings, sanctions, activePoints, suspension] = await Promise.all([ + loadUserWarnings(c, userId), + loadUserSanctions(c, userId), + loadActivePoints(c.get("db"), userId, now), + findActiveSuspension(c, userId, now), + ]); + const registry = c.get("core").moderation; + + const contentOf = async (row: (typeof warnings)[number]) => { + if (!row.pluginId || !row.targetType || !row.targetId) return null; + const target = findReportTarget(registry, { + pluginId: row.pluginId, + type: row.targetType, + }); + // Link only to content that is still there and not hidden. A plugin that + // fails to answer costs the member the link, never the page. + const current = target + ? await target.definition + .current(c, { locale: null, targetId: row.targetId }) + .catch(() => null) + : null; + + return { + title: row.contentSnapshot?.title ?? null, + url: current && !current.hidden ? current.url : null, + }; + }; + + return c.json( + { + suspension: suspension ? { endsAt: suspension.endsAt } : null, + activePoints, + warnings: await Promise.all( + warnings.map(async row => ({ + id: row.id, + status: warningStatusAt(row, now), + points: row.points, + reason: row.reasonSnapshot?.title ?? null, + message: row.message, + issuedAt: row.issuedAt, + expiresAt: row.expiresAt, + content: await contentOf(row), + })), + ), + sanctions: sanctions.map(row => ({ + id: row.id, + active: isSanctionActiveAt(row, now), + startsAt: row.startsAt, + endsAt: row.endsAt, + revoked: row.revokedAt !== null, + warningId: row.warningId, + })), + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts index 653e96b4d..3796bdf37 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/show.route.ts @@ -3,6 +3,7 @@ import { asc, eq } from "drizzle-orm"; import { buildRoute } from "@/api/builders"; import { canOnReport, requireModerator } from "@/api/lib/moderation/access"; +import { canWarnForContent } from "@/api/lib/moderation/warnings/access"; import { core_moderation_report_events } from "@/database/moderation"; import { MODERATION_PERMISSIONS, @@ -76,6 +77,8 @@ const detailSchema = z.object({ }), permissions: z.object({ manage: z.boolean(), + /** Whether the moderator may warn the content's authors from this report. */ + warn: z.boolean(), actions: z.array( z.object({ action: z.enum(MODERATION_TARGET_ACTIONS), @@ -217,6 +220,9 @@ export const showReportRoute = buildRoute({ }, permissions: { manage, + warn: + target?.definition.resolveAuthors !== undefined && + canWarnForContent(c, moderator.set, ref), actions: MODERATION_TARGET_ACTIONS.flatMap(action => { const definition = target?.definition.actions?.[action]; if (!definition) return []; diff --git a/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts new file mode 100644 index 000000000..1ad469db2 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/routes/warnings.routes.ts @@ -0,0 +1,720 @@ +import type { Context } from "hono"; + +import { z } from "@hono/zod-openapi"; +import { + and, + count, + desc, + eq, + gt, + gte, + ilike, + isNotNull, + isNull, + lte, + or, +} from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; + +import type { ModeratorContext } from "@/api/lib/moderation/access"; + +import { buildRoute } from "@/api/builders"; +import { listAuditLogsWithDetails } from "@/api/lib/audit/query"; +import { requireModerator } from "@/api/lib/moderation/access"; +import { + canWarnAnywhere, + hasWarningPermission, + viewableWarningsCondition, +} from "@/api/lib/moderation/warnings/access"; +import { + listWarningReasons, + warningReasonSchema, +} from "@/api/lib/moderation/warnings/reasons"; +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { core_users } from "@/database/users"; +import { core_moderation_warnings } from "@/database/warnings"; +import { containsLikePattern } from "@/lib/api/like-pattern"; +import { AUDIT_CORE_PLUGIN_ID } from "@/lib/audit/const"; +import { + WARNING_LIMITS, + WARNING_PERMISSIONS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import type { WarningDraftInput } from "../lib/warnings-service"; + +import { loadReportPeople } from "../lib/people"; +import { errorSchema, json, reportPersonSchema } from "../lib/schema"; +import { + issueWarning, + loadActivePoints, + projectWarning, + resolveWarningDraft, + revokeSanction, + revokeWarning, +} from "../lib/warnings-service"; +import { + loadUserSanctions, + loadUserWarnings, + sanctionSchema, + serializeSanctions, + serializeStaffWarnings, + staffWarningSchema, +} from "../lib/warnings-view"; + +const body = (schema: T) => ({ + body: { content: { "application/json": { schema } }, required: true }, +}); + +const idParams = z.object({ + id: z.coerce.number().int().positive().openapi({ example: 1 }), +}); + +const expirationSchema = z.discriminatedUnion("mode", [ + z.object({ mode: z.literal("default") }), + z.object({ mode: z.literal("never") }), + z.object({ + mode: z.literal("hours"), + hours: z.number().int().min(1).max(WARNING_LIMITS.maxDurationHours), + }), + z.object({ mode: z.literal("at"), at: z.iso.datetime() }), +]); + +const draftSchema = z.object({ + userId: z.number().int().positive(), + reasonId: z.number().int().positive().nullable(), + points: z.number().int().min(0).max(WARNING_LIMITS.maxPoints).nullish(), + expiration: expirationSchema.default({ mode: "default" }), + content: z + .object({ + pluginId: z.string().min(1).max(100), + targetType: z.string().min(1).max(100), + targetId: z.string().min(1).max(64), + }) + .nullish(), + reportId: z.number().int().positive().nullish(), +}); + +const issueSchema = draftSchema.extend({ + message: z + .string() + .trim() + .min(WARNING_LIMITS.messageMinLength) + .max(WARNING_LIMITS.messageMaxLength), + staffNote: z.string().trim().max(WARNING_LIMITS.noteMaxLength).nullish(), + overrideNote: z.string().trim().max(WARNING_LIMITS.noteMaxLength).nullish(), + resolveReport: z.boolean().default(false), + idempotencyKey: z + .string() + .min(8) + .max(64) + .regex(/^[\w-]+$/), +}); + +const revokeSchema = z.object({ + reason: z + .string() + .trim() + .min(WARNING_LIMITS.explanationMinLength) + .max(WARNING_LIMITS.noteMaxLength), +}); + +const toDraft = (input: z.infer): WarningDraftInput => ({ + content: input.content ?? null, + expiration: + input.expiration.mode === "at" + ? { at: new Date(input.expiration.at), mode: "at" } + : input.expiration, + points: input.points ?? null, + reasonId: input.reasonId, + reportId: input.reportId ?? null, + userId: input.userId, +}); + +const commonResponses = { + 400: json(errorSchema, "Invalid input"), + 401: { description: "Sign in first" }, + 403: json(errorSchema, "Not allowed for this moderator or this member"), + 404: json(errorSchema, "No such member, reason, content or report"), + 409: json(errorSchema, "The state changed, or the reason is disabled"), +}; + +const requireWarner = async (c: Context): Promise => { + const moderator = await requireModerator(c); + if (!canWarnAnywhere(c, moderator.set)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + return moderator; +}; + +export const listWarningReasonsRoute = buildRoute({ + route: { + method: "get", + path: "/warnings/reasons", + description: + "Enabled warning reasons, in display order, for the warn form (moderators who can warn)", + responses: { + 200: json( + z.object({ reasons: z.array(warningReasonSchema) }), + "Warning reasons", + ), + 401: commonResponses[401], + 403: { description: "Cannot warn anyone" }, + }, + }, + handler: async c => { + await requireWarner(c); + + return c.json( + { reasons: await listWarningReasons(c, { enabledOnly: true }) }, + 200, + ); + }, +}); + +const projectedSanctionSchema = z.object({ + action: z.enum(WARNING_THRESHOLD_ACTIONS), + durationHours: z.number().nullable(), + endsAt: z.date().nullable(), + points: z.number(), + thresholdId: z.number(), +}); + +export const previewWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings/preview", + description: + "Runs every check of a warning without issuing it and predicts its consequences: the member's active points before and after, and the suspension a threshold would apply. Advisory - issuing recalculates", + request: body(draftSchema), + responses: { + 200: json( + z.object({ + user: reportPersonSchema, + points: z.object({ + current: z.number(), + warning: z.number(), + projected: z.number(), + }), + defaults: z.object({ + points: z.number(), + expiresAfterHours: z.number().nullable(), + }), + expiresAt: z.date().nullable(), + overrides: z.object({ points: z.boolean(), expiration: z.boolean() }), + sanction: projectedSanctionSchema.nullable(), + suspendedUntil: z.object({ endsAt: z.date().nullable() }).nullable(), + permissions: z.object({ + overridePoints: z.boolean(), + overrideExpiration: z.boolean(), + resolveReport: z.boolean(), + viewWarnings: z.boolean(), + }), + related: z.array(staffWarningSchema), + }), + "What the warning would do", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireWarner(c); + const draft = await resolveWarningDraft( + c, + moderator, + toDraft(c.req.valid("json")), + ); + const now = new Date(); + const expiresAt = draft.expiresAt(now); + const projection = await projectWarning(c, { + expiresAt, + points: draft.points, + userId: draft.user.id, + }); + const canViewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + const [people, suspension, relatedRows] = await Promise.all([ + loadReportPeople(c, [draft.user.id]), + canViewSanctions ? findActiveSuspension(c, draft.user.id, now) : null, + draft.content + ? c + .get("db") + .select() + .from(core_moderation_warnings) + .where( + and( + eq(core_moderation_warnings.pluginId, draft.content.pluginId), + eq( + core_moderation_warnings.targetType, + draft.content.targetType, + ), + eq(core_moderation_warnings.targetId, draft.content.targetId), + ), + ) + .orderBy(desc(core_moderation_warnings.issuedAt)) + .limit(10) + : [], + ]); + const user = people.get(draft.user.id); + if (!user) throw new HTTPException(404, { message: "user_not_found" }); + + return c.json( + { + user, + points: { + current: projection.before, + warning: draft.points, + projected: projection.after, + }, + defaults: { + points: draft.defaultPoints, + expiresAfterHours: draft.defaultExpiresAfterHours, + }, + expiresAt, + overrides: draft.overrides, + sanction: projection.sanction, + suspendedUntil: suspension ? { endsAt: suspension.endsAt } : null, + permissions: { + overridePoints: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.overridePoints, + ), + overrideExpiration: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.overrideExpiration, + ), + resolveReport: + draft.report !== null && draft.report.status !== "done", + viewWarnings: hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.view, + ), + }, + related: await serializeStaffWarnings(c, moderator, relatedRows, now), + }, + 200, + ); + }, +}); + +export const issueWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings", + description: + "Issue a warning. The server resolves the content's author and scope, recalculates points under the member's lock, applies at most one threshold suspension and notifies the member. Retrying with the same `idempotencyKey` returns the first result", + request: body(issueSchema), + responses: { + 200: json( + z.object({ + warningId: z.number(), + duplicate: z.boolean(), + points: z.object({ before: z.number(), after: z.number() }), + reportResolved: z.boolean(), + sanction: z + .object({ + id: z.number(), + endsAt: z.date().nullable(), + thresholdId: z.number().nullable(), + }) + .nullable(), + }), + "The warning as issued", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireWarner(c); + const input = c.req.valid("json"); + const result = await issueWarning(c, moderator, { + ...toDraft(input), + idempotencyKey: input.idempotencyKey, + message: input.message, + overrideNote: input.overrideNote ?? null, + resolveReport: input.resolveReport, + staffNote: input.staffNote ?? null, + }); + + return c.json(result, 200); + }, +}); + +export const revokeWarningRoute = buildRoute({ + route: { + method: "post", + path: "/warnings/{id}/revoke", + description: + "Revoke a warning with a reason. Its points stop counting at once; the record and any suspension it led to stay", + request: { params: idParams, ...body(revokeSchema) }, + responses: { + 200: json( + z.object({ warningId: z.number(), userId: z.number() }), + "Warning revoked", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const { reason } = c.req.valid("json"); + + return c.json(await revokeWarning(c, moderator, { id, reason }), 200); + }, +}); + +export const revokeSanctionRoute = buildRoute({ + route: { + method: "post", + path: "/sanctions/{id}/revoke", + description: + "Lift one sanction with a reason. Other active sanctions of the member keep applying", + request: { params: idParams, ...body(revokeSchema) }, + responses: { + 200: json( + z.object({ sanctionId: z.number(), userId: z.number() }), + "Sanction revoked", + ), + ...commonResponses, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const { reason } = c.req.valid("json"); + + return c.json(await revokeSanction(c, moderator, { id, reason }), 200); + }, +}); + +const statusCondition = ( + status: "active" | "all" | "expired" | "revoked", + now: Date, +) => { + const table = core_moderation_warnings; + switch (status) { + case "active": + return and( + isNull(table.revokedAt), + or(isNull(table.expiresAt), gt(table.expiresAt, now)), + ); + case "all": + return undefined; + case "expired": + return and(isNull(table.revokedAt), lte(table.expiresAt, now)); + case "revoked": + return isNotNull(table.revokedAt); + } +}; + +export const listWarningsRoute = buildRoute({ + route: { + method: "get", + path: "/warnings", + description: + "Warnings the moderator may see, newest first, with filters. Moderators without `can_view` see the warnings they issued and those linked to content in their scope", + request: { + query: z.object({ + userId: z.coerce.number().int().positive().optional(), + reasonId: z.coerce.number().int().positive().optional(), + issuedById: z.coerce.number().int().positive().optional(), + status: z.enum(["all", "active", "expired", "revoked"]).default("all"), + pluginId: z.string().max(100).optional(), + targetType: z.string().max(100).optional(), + from: z.iso.datetime().optional(), + to: z.iso.datetime().optional(), + page: z.coerce.number().int().positive().max(10_000).default(1), + pageSize: z.coerce + .number() + .int() + .positive() + .max(100) + .default(WARNING_LIMITS.pageSize), + }), + }, + responses: { + 200: json( + z.object({ + items: z.array( + staffWarningSchema.extend({ user: reportPersonSchema.nullable() }), + ), + page: z.number(), + pageSize: z.number(), + total: z.number(), + }), + "One page of warnings", + ), + 401: commonResponses[401], + 403: { description: "Not a moderator" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const query = c.req.valid("query"); + const table = core_moderation_warnings; + const now = new Date(); + const where = and( + viewableWarningsCondition(c, moderator), + statusCondition(query.status, now), + query.userId ? eq(table.userId, query.userId) : undefined, + query.reasonId ? eq(table.reasonId, query.reasonId) : undefined, + query.issuedById ? eq(table.issuedById, query.issuedById) : undefined, + query.pluginId ? eq(table.pluginId, query.pluginId) : undefined, + query.targetType ? eq(table.targetType, query.targetType) : undefined, + query.from ? gte(table.issuedAt, new Date(query.from)) : undefined, + query.to ? lte(table.issuedAt, new Date(query.to)) : undefined, + ); + + const [[totalRow], rows] = await Promise.all([ + c.get("db").select({ value: count() }).from(table).where(where), + c + .get("db") + .select() + .from(table) + .where(where) + .orderBy(desc(table.issuedAt), desc(table.id)) + .limit(query.pageSize) + .offset((query.page - 1) * query.pageSize), + ]); + const [items, people] = await Promise.all([ + serializeStaffWarnings(c, moderator, rows, now), + loadReportPeople( + c, + rows.map(row => row.userId), + ), + ]); + + return c.json( + { + items: items.map(item => ({ + ...item, + user: people.get(item.userId) ?? null, + })), + page: query.page, + pageSize: query.pageSize, + total: totalRow?.value ?? 0, + }, + 200, + ); + }, +}); + +export const searchMembersRoute = buildRoute({ + route: { + method: "get", + path: "/members", + description: + "Find members by name for the ModCP member lookup (moderators who can warn or view warnings)", + request: { + query: z.object({ search: z.string().trim().min(1).max(100) }), + }, + responses: { + 200: json( + z.object({ items: z.array(reportPersonSchema) }), + "Up to 10 members", + ), + 401: commonResponses[401], + 403: { description: "Cannot warn or view warnings" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + if ( + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view) && + !hasWarningPermission(moderator.set, WARNING_PERMISSIONS.warn) + ) { + throw new HTTPException(403, { message: "Forbidden" }); + } + const { search } = c.req.valid("query"); + const pattern = containsLikePattern(search); + const rows = await c + .get("db") + .select({ id: core_users.id }) + .from(core_users) + .where( + or( + ilike(core_users.name, pattern), + ilike(core_users.nameCode, pattern), + ), + ) + .orderBy(core_users.name) + .limit(10); + const people = await loadReportPeople( + c, + rows.map(row => row.id), + ); + + return c.json( + { + items: rows.flatMap(row => { + const person = people.get(row.id); + + return person ? [person] : []; + }), + }, + 200, + ); + }, +}); + +export const memberStandingRoute = buildRoute({ + route: { + method: "get", + path: "/members/{id}", + description: + "A member's standing for the ModCP: global active points, the warnings this moderator may see and, with `can_view_sanctions`, their sanctions", + request: { params: idParams }, + responses: { + 200: json( + z.object({ + user: reportPersonSchema, + activePoints: z.number(), + hiddenWarnings: z.number(), + warnings: z.array(staffWarningSchema), + sanctions: z.array(sanctionSchema).nullable(), + permissions: z.object({ + warn: z.boolean(), + revoke: z.boolean(), + viewSanctions: z.boolean(), + manageSanctions: z.boolean(), + viewLog: z.boolean(), + }), + }), + "The member's standing", + ), + 401: commonResponses[401], + 403: { description: "Not a moderator" }, + 404: json(errorSchema, "No such member"), + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + const { id } = c.req.valid("param"); + const has = ( + permission: (typeof WARNING_PERMISSIONS)[keyof typeof WARNING_PERMISSIONS], + ) => hasWarningPermission(moderator.set, permission); + if (!has(WARNING_PERMISSIONS.view) && !canWarnAnywhere(c, moderator.set)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + const people = await loadReportPeople(c, [id]); + const user = people.get(id); + if (!user) return c.json({ error: "not_found" }, 404); + + const now = new Date(); + const viewSanctions = has(WARNING_PERMISSIONS.viewSanctions); + const [rows, sanctionRows, activePoints] = await Promise.all([ + loadUserWarnings(c, id), + viewSanctions ? loadUserSanctions(c, id) : null, + loadActivePoints(c.get("db"), id, now), + ]); + const warnings = await serializeStaffWarnings(c, moderator, rows, now); + + return c.json( + { + user, + activePoints, + hiddenWarnings: rows.length - warnings.length, + warnings, + sanctions: sanctionRows + ? await serializeSanctions(c, sanctionRows, now) + : null, + permissions: { + warn: has(WARNING_PERMISSIONS.warn) && id !== moderator.user.id, + revoke: has(WARNING_PERMISSIONS.revoke), + viewSanctions, + manageSanctions: has(WARNING_PERMISSIONS.manageSanctions), + viewLog: has(WARNING_PERMISSIONS.view), + }, + }, + 200, + ); + }, +}); + +const LOG_PAGE_SIZE = 20; + +export const memberLogRoute = buildRoute({ + route: { + method: "get", + path: "/members/{id}/log", + description: + "A member's moderation log for the ModCP, newest first: warnings issued and revoked and, with `can_view_sanctions`, suspensions applied and lifted. Read from the central activity log; needs `can_view`. Never includes staff notes", + request: { + params: idParams, + query: z.object({ cursor: z.string().max(200).optional() }), + }, + responses: { + 200: json( + z.object({ + edges: z.array( + z.object({ + id: z.number(), + action: z.string(), + actor: reportPersonSchema.nullable(), + actorName: z.string().nullable(), + createdAt: z.date(), + metadata: z.record(z.string(), z.unknown()), + }), + ), + pageInfo: z.object({ + endCursor: z.string().nullable(), + hasNextPage: z.boolean(), + }), + }), + "One page of the member's moderation log", + ), + 401: commonResponses[401], + 403: { description: "Needs `can_view` warnings" }, + }, + }, + handler: async c => { + const moderator = await requireModerator(c); + if (!hasWarningPermission(moderator.set, WARNING_PERMISSIONS.view)) { + throw new HTTPException(403, { message: "Forbidden" }); + } + const { id } = c.req.valid("param"); + const { cursor } = c.req.valid("query"); + const viewSanctions = hasWarningPermission( + moderator.set, + WARNING_PERMISSIONS.viewSanctions, + ); + + const { edges, pageInfo } = await listAuditLogsWithDetails(c, { + cursor, + filters: { + area: "moderator", + pluginId: AUDIT_CORE_PLUGIN_ID, + targetId: String(id), + targetType: "user", + }, + first: LOG_PAGE_SIZE, + }); + const visible = edges.filter( + entry => viewSanctions || !entry.action.startsWith("sanction."), + ); + const people = await loadReportPeople( + c, + visible.map(entry => entry.actor?.id), + ); + + return c.json( + { + edges: visible.map(entry => ({ + id: entry.id, + action: entry.action, + actor: entry.actor ? (people.get(entry.actor.id) ?? null) : null, + actorName: entry.actorName, + createdAt: entry.createdAt, + metadata: entry.metadata, + })), + pageInfo, + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts new file mode 100644 index 000000000..3ba0f9238 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/warnings.postgres.test.ts @@ -0,0 +1,202 @@ +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { asc, eq } from "drizzle-orm"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import type { PostgresTestSchema } from "@/tests/postgres"; + +import { core_languages, core_languages_words } from "@/database/languages"; +import { core_roles } from "@/database/roles"; +import { core_users } from "@/database/users"; +import { + core_moderation_sanctions, + core_moderation_warning_reasons, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { resolvePersonalInformationFields } from "@/lib/user-personal-information"; +import { manifest } from "@/manifest"; +import { createTestCache } from "@/tests/cache"; +import { createPostgresTestSchema, POSTGRES_TEST_URL } from "@/tests/postgres"; +import { grantStaffPermissions } from "@/tests/staff-permissions"; + +import { moderationModule } from "./moderation.module"; + +const MODERATOR = 1; +const CONCURRENT_MEMBER = 2; +const RETRIED_MEMBER = 3; +const USERS = [MODERATOR, CONCURRENT_MEMBER, RETRIED_MEMBER]; + +interface Issued { + duplicate: boolean; + sanction: null | { id: number }; + warningId: number; +} + +describe.skipIf(!POSTGRES_TEST_URL)("issuing warnings on PostgreSQL", () => { + let schema: PostgresTestSchema; + let reasonId: number; + const thresholds: Record<"eight" | "ten", number> = { eight: 0, ten: 0 }; + const cache = createTestCache(); + const publish = vi.fn(async (_args: unknown) => Promise.resolve()); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("user", { + id: Number(c.req.header("x-user")), + } as unknown as Context["var"]["user"]); + c.set("suspension", null); + c.set("cache", cache); + c.set("db", schema.db as unknown as Context["var"]["db"]); + c.set("events", { + emit: async () => Promise.resolve(), + } as unknown as Context["var"]["events"]); + c.set("notifications", { + publish, + } as unknown as Context["var"]["notifications"]); + c.set("realtime", { + closeUserConnections: () => undefined, + } as unknown as Context["var"]["realtime"]); + c.set("core", { + moderation: { scopes: [], targets: [] }, + permissionStaff: [], + personalInformationFields: resolvePersonalInformationFields(), + } as unknown as Context["var"]["core"]); + await next(); + }); + app.route("/", moderationModule.hono); + + const issue = async (userId: number, idempotencyKey: string) => { + const res = await app.request("/warnings", { + body: JSON.stringify({ + idempotencyKey, + message: "Please keep to the topic.", + reasonId, + userId, + }), + headers: { + "content-type": "application/json", + "x-user": String(MODERATOR), + }, + method: "POST", + }); + expect(res.status).toBe(200); + + return (await res.json()) as Issued; + }; + + beforeAll(async () => { + schema = await createPostgresTestSchema({ prefix: "vitnode_warnings" }); + await schema.db + .insert(core_languages) + .values({ code: "en", name: "English" }); + const [role] = await schema.db + .insert(core_roles) + .values({ updatedAt: new Date() }) + .returning({ id: core_roles.id }); + await schema.db.insert(core_users).values( + USERS.map(id => ({ + avatarColor: "000000", + email: `user${id}@example.com`, + id, + ipAddress: "127.0.0.1", + name: `user${id}`, + nameCode: `user${id}`, + roleId: role.id, + })), + ); + const [reason] = await schema.db + .insert(core_moderation_warning_reasons) + .values({ expiresAfterHours: null, points: 2 }) + .returning({ id: core_moderation_warning_reasons.id }); + reasonId = reason.id; + await schema.db.insert(core_languages_words).values({ + itemId: reasonId, + languageCode: "en", + pluginCode: "core", + tableName: "core_moderation_warning_reasons", + value: "Off-topic posting", + variable: "title", + }); + const rows = await schema.db + .insert(core_moderation_warning_thresholds) + .values([ + { action: "suspend_temporary", durationHours: 24, points: 8 }, + { action: "suspend_indefinite", durationHours: null, points: 10 }, + ]) + .returning({ id: core_moderation_warning_thresholds.id }); + thresholds.eight = rows[0]?.id ?? 0; + thresholds.ten = rows[1]?.id ?? 0; + await grantStaffPermissions(cache, { + permissions: Object.values(WARNING_PERMISSIONS).map(permission => ({ + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + })), + type: "moderator", + userId: MODERATOR, + }); + }, 120_000); + + afterAll(async () => { + await schema?.drop(); + }); + + const warningsOf = async (userId: number) => + await schema.db + .select({ + pointsAfter: core_moderation_warnings.pointsAfter, + pointsBefore: core_moderation_warnings.pointsBefore, + }) + .from(core_moderation_warnings) + .where(eq(core_moderation_warnings.userId, userId)) + .orderBy(asc(core_moderation_warnings.pointsBefore)); + + const sanctionsOf = async (userId: number) => + await schema.db + .select({ thresholdId: core_moderation_sanctions.thresholdId }) + .from(core_moderation_sanctions) + .where(eq(core_moderation_sanctions.userId, userId)) + .orderBy(asc(core_moderation_sanctions.thresholdId)); + + it("chains points across concurrent warnings and applies each crossed threshold once", async () => { + await Promise.all( + [1, 2, 3, 4, 5].map( + async n => await issue(CONCURRENT_MEMBER, `concurrent-${n}`), + ), + ); + + expect(await warningsOf(CONCURRENT_MEMBER)).toEqual([ + { pointsAfter: 2, pointsBefore: 0 }, + { pointsAfter: 4, pointsBefore: 2 }, + { pointsAfter: 6, pointsBefore: 4 }, + { pointsAfter: 8, pointsBefore: 6 }, + { pointsAfter: 10, pointsBefore: 8 }, + ]); + expect(await sanctionsOf(CONCURRENT_MEMBER)).toEqual([ + { thresholdId: thresholds.eight }, + { thresholdId: thresholds.ten }, + ]); + }); + + it("turns concurrent retries of one request into a single warning", async () => { + publish.mockClear(); + + const results = await Promise.all( + Array.from( + { length: 4 }, + async () => await issue(RETRIED_MEMBER, "retried-request"), + ), + ); + + expect(results.filter(result => !result.duplicate)).toHaveLength(1); + expect(new Set(results.map(result => result.warningId)).size).toBe(1); + expect(await warningsOf(RETRIED_MEMBER)).toHaveLength(1); + expect(publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/vitnode/src/api/modules/moderation/warnings.test.ts b/packages/vitnode/src/api/modules/moderation/warnings.test.ts new file mode 100644 index 000000000..d04338473 --- /dev/null +++ b/packages/vitnode/src/api/modules/moderation/warnings.test.ts @@ -0,0 +1,1045 @@ +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "@/api/lib/permission-staff"; + +import { findActiveSuspension } from "@/api/lib/moderation/warnings/suspension"; +import { + accountSuspendedNotification, + warningIssuedNotification, +} from "@/api/lib/notifications/core-types"; +import { defineModerationScope, defineReportTarget } from "@/api/moderation"; +import { core_audit_logs } from "@/database/audit"; +import { core_languages_words } from "@/database/languages"; +import { + core_moderation_report_events, + core_moderation_report_reasons, + core_moderation_reports, +} from "@/database/moderation"; +import { core_roles } from "@/database/roles"; +import { core_users } from "@/database/users"; +import { + core_moderation_sanctions, + core_moderation_warning_reasons, + core_moderation_warning_thresholds, + core_moderation_warnings, +} from "@/database/warnings"; +import { + WARNING_PERMISSION_MODULE, + WARNING_PERMISSIONS, +} from "@/lib/moderation/warnings"; +import { resolvePersonalInformationFields } from "@/lib/user-personal-information"; +import { manifest } from "@/manifest"; +import { createTestCache } from "@/tests/cache"; +import { createMemoryDb } from "@/tests/memory-db"; +import { + grantStaffPermissions, + ROOT_STAFF_PERMISSIONS, +} from "@/tests/staff-permissions"; + +import { moderationModule } from "./moderation.module"; + +const FORUM = "@vitnode/forum"; +const FAMILY = "forum.section"; +const TYPE = "forum.topic"; +const SECTION_A = "10"; +const SECTION_B = "20"; + +const MEMBER_ROLE = 3; +const ROOT_ROLE = 4; + +const MEMBER = 100; +const OTHER_MEMBER = 101; +const ROOT_ROLE_MEMBER = 102; +const SENIOR = 200; +const BASIC = 201; +const VIEWER = 202; +const SCOPED = 203; +const ROOT_MOD = 300; + +const REASON_TWO = 1; +const REASON_FIVE = 2; +const REASON_ZERO = 3; +const REASON_OFF = 4; +const TITLE_OF_TWO = "Off-topic posting"; + +const THRESHOLD_EIGHT = 1; +const THRESHOLD_TEN = 2; + +const HOUR = 60 * 60 * 1000; + +const STAFF_NOTE = "staff-only-note"; +const OVERRIDE_NOTE = "override-explanation"; + +const warnings = (...permissions: string[]): PermissionsStaffArgs[] => + permissions.map(permission => ({ + module: WARNING_PERMISSION_MODULE, + permission, + plugin: manifest.id, + })); + +const sectionA = (permission: string): PermissionsStaffArgs => ({ + module: "topics", + permission, + plugin: FORUM, + scope: { family: FAMILY, ids: [SECTION_A], mode: "selected" }, +}); + +let keys = 0; +const nextKey = () => `warn-key-${++keys}`; + +const harness = async () => { + const sections = new Map([ + ["1", SECTION_A], + ["3", SECTION_B], + ]); + const authorOf = (targetId: string) => (targetId === "4" ? null : MEMBER); + + const target = defineReportTarget({ + capture: async (_c, { targetId }) => + Promise.resolve({ + authors: [{ id: MEMBER, name: "Member" }], + fields: [{ kind: "text", label: null, name: "body", value: "Body" }], + locale: { fallback: false, requested: null, served: "en" }, + scope: null, + source: { publishedAt: null, updatedAt: null, version: null }, + title: `Topic ${targetId}`, + url: `/topics/${targetId}`, + }), + current: async (_c, { targetId }) => + Promise.resolve({ hidden: false, url: `/topics/${targetId}` }), + label: "Topic", + parseId: value => (/^\d+$/.test(value) ? value : null), + permissionModule: "topics", + resolveAuthors: async (_c, { targetId }) => { + const author = authorOf(targetId); + + return Promise.resolve( + author === null + ? null + : { authorIds: [author], title: `Topic ${targetId}` }, + ); + }, + resolveScope: async (_c, targetId) => + Promise.resolve(sections.get(targetId)), + scope: FAMILY, + type: TYPE, + }); + const scope = defineModerationScope({ + find: async () => Promise.resolve([]), + id: FAMILY, + label: "Section", + list: async () => Promise.resolve({ items: [], nextCursor: null }), + }); + + const cache = createTestCache(); + const grants: [number, PermissionsStaffArgs[] | StaffPermissionSet][] = [ + [SENIOR, warnings(...Object.values(WARNING_PERMISSIONS))], + [BASIC, warnings(WARNING_PERMISSIONS.warn)], + [VIEWER, warnings(WARNING_PERMISSIONS.view)], + [ + SCOPED, + [ + sectionA("can_warn"), + sectionA("can_view_reports"), + sectionA("can_manage_reports"), + ], + ], + [ROOT_MOD, ROOT_STAFF_PERMISSIONS], + ]; + for (const [userId, permissions] of grants) { + await grantStaffPermissions(cache, { + permissions, + type: "moderator", + userId, + }); + } + + const users = [ + MEMBER, + OTHER_MEMBER, + ROOT_ROLE_MEMBER, + SENIOR, + BASIC, + VIEWER, + SCOPED, + ROOT_MOD, + ]; + const reasons = [ + { enabled: true, expiresAfterHours: 720, id: REASON_TWO, points: 2 }, + { enabled: true, expiresAfterHours: null, id: REASON_FIVE, points: 5 }, + { enabled: true, expiresAfterHours: null, id: REASON_ZERO, points: 0 }, + { enabled: false, expiresAfterHours: null, id: REASON_OFF, points: 3 }, + ]; + const memory = createMemoryDb([ + [ + core_users, + users.map(id => ({ + email: `${id}@example.com`, + id, + name: `User ${id}`, + nameCode: `user-${id}`, + roleId: id === ROOT_ROLE_MEMBER ? ROOT_ROLE : MEMBER_ROLE, + })), + ], + [ + core_roles, + [ + { color: null, id: MEMBER_ROLE, prefix: null, root: false }, + { color: null, id: ROOT_ROLE, prefix: null, root: true }, + ], + ], + [ + core_moderation_report_reasons, + [{ code: "spam", id: 1, position: 0, requiresDetails: false }], + ], + [ + core_moderation_warning_reasons, + reasons.map(reason => ({ ...reason, position: reason.id })), + ], + [ + core_languages_words, + reasons.map(reason => ({ + id: reason.id, + itemId: reason.id, + languageCode: "en", + pluginCode: "core", + tableName: "core_moderation_warning_reasons", + value: reason.id === REASON_TWO ? TITLE_OF_TWO : `Reason ${reason.id}`, + variable: "title", + })), + ], + [ + core_moderation_warning_thresholds, + [ + { + action: "suspend_temporary", + durationHours: 24, + enabled: true, + id: THRESHOLD_EIGHT, + points: 8, + }, + { + action: "suspend_indefinite", + durationHours: null, + enabled: true, + id: THRESHOLD_TEN, + points: 10, + }, + ], + ], + ]); + const emit = vi.fn(async (_name: string, _payload: unknown) => + Promise.resolve(), + ); + const publish = vi.fn(async (_args: Record) => + Promise.resolve(), + ); + const closeUserConnections = vi.fn((_userId: number) => undefined); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + const userId = Number(c.req.header("x-user")); + const suspendedId = Number(c.req.header("x-suspended")); + c.set( + "user", + (users.includes(userId) + ? { id: userId, roleId: MEMBER_ROLE } + : null) as unknown as Context["var"]["user"], + ); + c.set( + "suspension", + suspendedId ? { endsAt: null, userId: suspendedId } : null, + ); + c.set("cache", cache); + c.set("db", memory.db as unknown as Context["var"]["db"]); + c.set("events", { emit } as unknown as Context["var"]["events"]); + c.set("notifications", { + publish, + } as unknown as Context["var"]["notifications"]); + c.set("realtime", { + closeUserConnections, + } as unknown as Context["var"]["realtime"]); + c.set("core", { + moderation: { + scopes: [{ definition: scope, pluginId: FORUM }], + targets: [{ definition: target, pluginId: FORUM }], + }, + permissionStaff: [], + personalInformationFields: resolvePersonalInformationFields(), + } as unknown as Context["var"]["core"]); + await next(); + }); + app.route("/", moderationModule.hono); + + const call = async ( + userId: number, + method: "GET" | "POST", + path: string, + body?: Record, + headers: Record = {}, + ) => + await app.request(path, { + method, + headers: { + "content-type": "application/json", + "x-user": String(userId), + ...headers, + }, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + + const issue = async (userId: number, body: Record = {}) => + await call(userId, "POST", "/warnings", { + idempotencyKey: nextKey(), + message: "Please keep to the topic.", + reasonId: REASON_TWO, + userId: MEMBER, + ...body, + }); + + const issued = async (userId: number, body: Record = {}) => { + const res = await issue(userId, body); + expect(res.status).toBe(200); + + return (await res.json()) as Issued; + }; + + const revoke = async ( + userId: number, + kind: "sanctions" | "warnings", + id: number, + ) => + await call(userId, "POST", `/${kind}/${id}/revoke`, { + reason: "Issued by mistake", + }); + + /** Active points the member already holds, as one warning outside the API. */ + const seedPoints = async (userId: number, points: number) => { + await memory.db.insert(core_moderation_warnings).values({ + issuedById: SENIOR, + message: "Earlier warning", + points, + userId, + }); + }; + + const standing = async (moderatorId: number, userId = MEMBER) => { + const res = await call(moderatorId, "GET", `/members/${userId}`); + expect(res.status).toBe(200); + + return (await res.json()) as Standing; + }; + + const suspensionOf = async (userId: number) => + await findActiveSuspension( + { + get: (key: string) => + key === "cache" ? cache : key === "db" ? memory.db : undefined, + } as unknown as Context, + userId, + ); + + const warningRows = () => memory.rows(core_moderation_warnings); + const sanctionRows = () => memory.rows(core_moderation_sanctions); + const reportEvents = (id: number) => + memory + .rows(core_moderation_report_events) + .filter(entry => entry.reportId === id) + .map(entry => entry.type); + + const fileReport = async (targetId: string) => { + const res = await call(OTHER_MEMBER, "POST", "/reports", { + pluginId: FORUM, + reason: "spam", + targetId, + targetType: TYPE, + }); + expect(res.status).toBe(201); + + return ((await res.json()) as { id: number }).id; + }; + const reportRow = (id: number) => + memory.rows(core_moderation_reports).find(entry => entry.id === id); + + return { + call, + closeUserConnections, + emit, + fileReport, + issue, + issued, + memory, + publish, + reportEvents, + reportRow, + revoke, + sanctionRows, + seedPoints, + standing, + suspensionOf, + warningRows, + }; +}; + +interface Issued { + duplicate: boolean; + points: { after: number; before: number }; + reportResolved: boolean; + sanction: null | { endsAt: null | string; id: number; thresholdId: number }; + warningId: number; +} + +interface StaffWarning { + id: number; + overrideNote: null | string; + reason: null | { languageCode: string; value: string }[]; + revokeReason: null | string; + staffNote: null | string; + status: string; +} + +interface Standing { + activePoints: number; + hiddenWarnings: number; + permissions: { viewLog: boolean }; + warnings: StaffWarning[]; +} + +interface MemberLog { + edges: { + action: string; + actor: null | { id: number }; + metadata: Record; + }[]; + pageInfo: { endCursor: null | string; hasNextPage: boolean }; +} + +interface AccountStatus { + activePoints: number; + sanctions: { active: boolean; id: number }[]; + suspension: null | { endsAt: null | string }; + warnings: Record[]; +} + +const accountStatus = async ( + h: Awaited>, + userId = MEMBER, +) => { + const res = await h.call(userId, "GET", "/account-status"); + expect(res.status).toBe(200); + + return (await res.json()) as AccountStatus; +}; + +const inHours = (hours: number) => + new Date(Date.now() + hours * HOUR).toISOString(); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("issuing a warning", () => { + it("takes points and expiry from the reason and keeps its title after the reason changes", async () => { + const h = await harness(); + + const { points, warningId } = await h.issued(SENIOR); + + expect(points).toEqual({ after: 2, before: 0 }); + const [row] = h.warningRows(); + expect(row).toMatchObject({ + defaultPoints: 2, + id: warningId, + overrideNote: null, + points: 2, + }); + expect( + (row.expiresAt as Date).getTime() - (row.issuedAt as Date).getTime(), + ).toBe(720 * HOUR); + + await h.memory.db + .update(core_languages_words) + .set({ value: "Renamed" }) + .where(eq(core_languages_words.itemId, REASON_TWO)); + await h.memory.db + .update(core_moderation_warning_reasons) + .set({ enabled: false }) + .where(eq(core_moderation_warning_reasons.id, REASON_TWO)); + + const original = [{ languageCode: "en", value: TITLE_OF_TWO }]; + expect((await h.standing(SENIOR)).warnings[0]?.reason).toEqual(original); + expect((await accountStatus(h)).warnings[0]?.reason).toEqual(original); + }); + + it("issues a zero-point warning that notifies the member without moving their points", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 7); + + const result = await h.issued(SENIOR, { reasonId: REASON_ZERO }); + + expect(result.points).toEqual({ after: 7, before: 7 }); + expect(result.sanction).toBeNull(); + expect(h.publish).toHaveBeenCalledTimes(1); + expect(h.publish.mock.calls[0]?.[0]).toMatchObject({ + recipients: [MEMBER], + type: warningIssuedNotification, + }); + }); + + it("accepts a points and expiration override with its explanation", async () => { + const h = await harness(); + + await h.issued(SENIOR, { + expiration: { hours: 48, mode: "hours" }, + overrideNote: OVERRIDE_NOTE, + points: 4, + }); + + const [row] = h.warningRows(); + expect(row).toMatchObject({ + defaultPoints: 2, + overrideNote: OVERRIDE_NOTE, + points: 4, + }); + expect( + (row.expiresAt as Date).getTime() - (row.issuedAt as Date).getTime(), + ).toBe(48 * HOUR); + expect( + h.memory + .rows(core_audit_logs) + .find(entry => entry.action === "warning.issued"), + ).toMatchObject({ + area: "moderator", + metadata: { defaultPoints: 2, points: 4, pointsOverridden: true }, + targetType: "user", + }); + }); + + it("lets a root moderator warn other staff", async () => { + const h = await harness(); + + expect((await h.issue(ROOT_MOD, { userId: SCOPED })).status).toBe(200); + }); + + it.each([ + [ + "a points override without permission", + BASIC, + { overrideNote: OVERRIDE_NOTE, points: 4 }, + 403, + "points_override_forbidden", + ], + [ + "a points override without explanation", + SENIOR, + { points: 4 }, + 400, + "override_note_required", + ], + [ + "an expiration override without permission", + BASIC, + { expiration: { mode: "never" }, overrideNote: OVERRIDE_NOTE }, + 403, + "expiration_override_forbidden", + ], + [ + "an expiration in the past", + SENIOR, + { expiration: { at: inHours(-1), mode: "at" }, overrideNote: "Past" }, + 400, + "invalid_expiration", + ], + [ + "an expiration beyond the longest allowed", + SENIOR, + { + expiration: { at: inHours(24 * 365 * 6), mode: "at" }, + overrideNote: "Far", + }, + 400, + "invalid_expiration", + ], + [ + "a disabled reason", + SENIOR, + { reasonId: REASON_OFF }, + 409, + "reason_disabled", + ], + ["a moderator's own account", SENIOR, { userId: SENIOR }, 403, "self"], + ["another staff member", BASIC, { userId: SCOPED }, 403, "protected"], + [ + "a root-role member, even by a root moderator", + ROOT_MOD, + { userId: ROOT_ROLE_MEMBER }, + 403, + "protected", + ], + ["a direct warning without core can_warn", SCOPED, {}, 403, "forbidden"], + [ + "content outside the moderator's scope", + SCOPED, + { content: { pluginId: FORUM, targetId: "3", targetType: TYPE } }, + 403, + "forbidden", + ], + [ + "a member who did not write the content", + SCOPED, + { + content: { pluginId: FORUM, targetId: "1", targetType: TYPE }, + userId: OTHER_MEMBER, + }, + 400, + "author_mismatch", + ], + ])("refuses %s", async (_name, actor, body, status, error) => { + const h = await harness(); + + const res = await h.issue(actor, body); + + expect(res.status).toBe(status); + expect(await res.json()).toMatchObject({ error }); + expect(h.warningRows()).toEqual([]); + expect(h.publish).not.toHaveBeenCalled(); + }); + + it("lets a scoped moderator warn an author, with the scope resolved on the server", async () => { + const h = await harness(); + + await h.issued(SCOPED, { + content: { + pluginId: FORUM, + scopeId: SECTION_B, + targetId: "1", + targetType: TYPE, + }, + reasonId: REASON_ZERO, + }); + + expect(h.warningRows()[0]).toMatchObject({ + contentSnapshot: { title: "Topic 1", url: "/topics/1" }, + scopeFamily: FAMILY, + scopeId: SECTION_A, + targetId: "1", + }); + }); +}); + +describe("thresholds", () => { + it.each([ + [REASON_TWO, 8, THRESHOLD_EIGHT, 24], + [REASON_FIVE, 11, THRESHOLD_TEN, null], + ])( + "reason %i takes 6 points to %i and applies only threshold %i", + async (reasonId, after, thresholdId, hours) => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + expect((await accountStatus(h)).suspension).toBeNull(); + + const result = await h.issued(SENIOR, { reasonId }); + + expect(result.points).toEqual({ after, before: 6 }); + expect(result.sanction?.thresholdId).toBe(thresholdId); + const sanctions = h.sanctionRows(); + expect(sanctions).toHaveLength(1); + const [sanction] = sanctions; + expect(sanction).toMatchObject({ + source: "threshold", + thresholdId, + userId: MEMBER, + warningId: result.warningId, + }); + expect( + hours === null + ? sanction.endsAt + : (sanction.endsAt as Date).getTime() - + (sanction.startsAt as Date).getTime(), + ).toBe(hours === null ? null : hours * HOUR); + + const status = await accountStatus(h); + expect(status.suspension).toEqual({ + endsAt: (sanction.endsAt as Date | null)?.toISOString() ?? null, + }); + expect(h.closeUserConnections).toHaveBeenCalledWith(MEMBER); + expect(h.publish.mock.calls.map(([args]) => args.type)).toEqual([ + warningIssuedNotification, + accountSuspendedNotification, + ]); + }, + ); + + it("answers a retry with the first result and writes nothing again", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const idempotencyKey = nextKey(); + + const first = await h.issued(SENIOR, { idempotencyKey }); + const writes = { + emits: h.emit.mock.calls.length, + publishes: h.publish.mock.calls.length, + }; + const retry = await h.issued(SENIOR, { idempotencyKey }); + + expect(retry).toEqual({ + ...first, + duplicate: true, + sanction: { ...first.sanction }, + }); + expect(h.warningRows()).toHaveLength(2); + expect(h.sanctionRows()).toHaveLength(1); + expect(h.emit).toHaveBeenCalledTimes(writes.emits); + expect(h.publish).toHaveBeenCalledTimes(writes.publishes); + + const other = await h.issue(SENIOR, { + idempotencyKey, + userId: OTHER_MEMBER, + }); + expect(other.status).toBe(409); + expect(await other.json()).toEqual({ error: "idempotency_conflict" }); + }); + + it("serializes concurrent warnings for one member", async () => { + const h = await harness(); + const repeated = nextKey(); + + const results = await Promise.all( + [repeated, nextKey(), nextKey(), nextKey(), nextKey(), repeated].map( + async idempotencyKey => await h.issued(SENIOR, { idempotencyKey }), + ), + ); + + expect(results.filter(result => result.duplicate)).toHaveLength(1); + expect( + h + .warningRows() + .map(row => [row.pointsBefore, row.pointsAfter]) + .sort(([a], [b]) => Number(a) - Number(b)), + ).toEqual([ + [0, 2], + [2, 4], + [4, 6], + [6, 8], + [8, 10], + ]); + expect( + h + .sanctionRows() + .map(row => row.thresholdId) + .sort((a, b) => Number(a) - Number(b)), + ).toEqual([THRESHOLD_EIGHT, THRESHOLD_TEN]); + }); + + it("does not suspend a member already above a threshold that is lowered", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 9); + await h.memory.db + .update(core_moderation_warning_thresholds) + .set({ points: 9 }) + .where(eq(core_moderation_warning_thresholds.id, THRESHOLD_TEN)); + + await h.issued(SENIOR, { reasonId: REASON_ZERO }); + await h.issued(SENIOR); + + expect(h.sanctionRows()).toEqual([]); + }); + + it("applies a threshold again once a revoked warning takes the member back under it", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + + const first = await h.issued(SENIOR); + expect((await h.revoke(SENIOR, "warnings", first.warningId)).status).toBe( + 200, + ); + const second = await h.issued(SENIOR); + + expect(second.points).toEqual({ after: 8, before: 6 }); + expect(h.sanctionRows().map(row => row.thresholdId)).toEqual([ + THRESHOLD_EIGHT, + THRESHOLD_EIGHT, + ]); + }); + + it("stops a temporary suspension once it ends, with no job involved", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-28T12:00:00.000Z")); + const h = await harness(); + await h.seedPoints(MEMBER, 6); + await h.issued(SENIOR); + expect((await accountStatus(h)).suspension).toEqual({ + endsAt: "2026-10-29T12:00:00.000Z", + }); + + vi.setSystemTime(new Date("2026-10-29T12:00:00.001Z")); + + const status = await accountStatus(h); + expect(status.suspension).toBeNull(); + expect(status.sanctions.map(sanction => sanction.active)).toEqual([false]); + }); +}); + +describe("revoking", () => { + it("removes a warning's points but keeps its record and the suspension it caused", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { warningId } = await h.issued(BASIC); + + expect((await h.revoke(BASIC, "warnings", warningId)).status).toBe(403); + expect((await h.revoke(SENIOR, "warnings", warningId)).status).toBe(200); + const again = await h.revoke(SENIOR, "warnings", warningId); + expect(again.status).toBe(409); + + const { activePoints, warnings: list } = await h.standing(SENIOR); + expect(activePoints).toBe(6); + expect(list.find(warning => warning.id === warningId)).toMatchObject({ + revokeReason: "Issued by mistake", + status: "revoked", + }); + expect(await h.suspensionOf(MEMBER)).not.toBeNull(); + }); + + it("lifts one suspension and leaves the others in force", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const temporary = await h.issued(SENIOR); + const indefinite = await h.issued(SENIOR); + expect((await h.suspensionOf(MEMBER))?.endsAt).toBeNull(); + const indefiniteId = indefinite.sanction?.id ?? 0; + + expect((await h.revoke(BASIC, "sanctions", indefiniteId)).status).toBe(403); + expect((await h.revoke(SENIOR, "sanctions", indefiniteId)).status).toBe( + 200, + ); + + expect(await h.suspensionOf(MEMBER)).toEqual({ + endsAt: new Date(temporary.sanction?.endsAt ?? 0), + sanctionIds: [temporary.sanction?.id], + }); + }); +}); + +describe("member moderation log", () => { + it.each([ + ["with can_view_sanctions", SENIOR, true], + ["without can_view_sanctions", VIEWER, false], + ])( + "lists the member's warning activity newest first %s", + async (_name, viewer, viewSanctions) => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { sanction, warningId } = await h.issued(SENIOR); + await h.issued(SENIOR, { reasonId: REASON_ZERO, userId: OTHER_MEMBER }); + expect((await h.revoke(SENIOR, "warnings", warningId)).status).toBe(200); + const sanctionId = sanction?.id ?? 0; + expect((await h.revoke(SENIOR, "sanctions", sanctionId)).status).toBe( + 200, + ); + + const res = await h.call(viewer, "GET", `/members/${MEMBER}/log`); + + expect(res.status).toBe(200); + const log = (await res.json()) as MemberLog; + expect(log.edges).toEqual( + [ + { action: "sanction.revoked", metadata: { sanctionId, warningId } }, + { action: "warning.revoked", metadata: { points: 2, warningId } }, + { + action: "sanction.applied", + metadata: { sanctionId, thresholdId: THRESHOLD_EIGHT, warningId }, + }, + { + action: "warning.issued", + metadata: { + appliedThresholdId: THRESHOLD_EIGHT, + points: 2, + pointsAfter: 8, + pointsBefore: 6, + }, + }, + ] + .filter(entry => viewSanctions || entry.action.startsWith("warning.")) + .map(({ action, metadata }) => + expect.objectContaining({ + action, + actor: expect.objectContaining({ id: SENIOR }), + metadata: expect.objectContaining(metadata), + }), + ), + ); + expect(log.pageInfo).toEqual({ + endCursor: expect.any(String), + hasNextPage: false, + }); + }, + ); + + it.each([ + ["can_view", 200, VIEWER], + ["only core can_warn", 403, BASIC], + ["only content-scoped can_warn", 403, SCOPED], + ])( + "answers a moderator with %s with %i and says so in the standing", + async (_name, status, moderator) => { + const h = await harness(); + + const res = await h.call(moderator, "GET", `/members/${MEMBER}/log`); + + expect(res.status).toBe(status); + expect((await h.standing(moderator)).permissions.viewLog).toBe( + status === 200, + ); + }, + ); +}); + +describe("reports", () => { + it("links a warning to a report and resolves it only when asked", async () => { + const h = await harness(); + const reportId = await h.fileReport("1"); + + const wrongUser = await h.issue(SCOPED, { reportId, userId: OTHER_MEMBER }); + expect(wrongUser.status).toBe(400); + expect(await wrongUser.json()).toEqual({ error: "author_mismatch" }); + + const linked = await h.issued(SCOPED, { + reasonId: REASON_ZERO, + reportId, + }); + expect(linked.reportResolved).toBe(false); + expect(h.reportRow(reportId)?.status).not.toBe("done"); + expect(h.reportEvents(reportId)).toEqual(["created", "warning"]); + + const resolving = await h.issued(SCOPED, { + reasonId: REASON_ZERO, + reportId, + resolveReport: true, + }); + expect(resolving.reportResolved).toBe(true); + expect(h.reportRow(reportId)).toMatchObject({ + resolution: "action_taken", + status: "done", + }); + expect(h.reportEvents(reportId)).toEqual([ + "created", + "warning", + "warning", + "resolved", + ]); + expect(h.warningRows().map(row => row.reportId)).toEqual([ + reportId, + reportId, + ]); + }); + + it("refuses to resolve a closed report and writes nothing", async () => { + const h = await harness(); + const reportId = await h.fileReport("1"); + const closed = await h.call( + SCOPED, + "POST", + `/reports/${reportId}/resolve`, + { + expectedVersion: h.reportRow(reportId)?.version, + resolution: "no_violation", + }, + ); + expect(closed.status).toBe(200); + const events = h.reportEvents(reportId); + + const res = await h.issue(SCOPED, { + reasonId: REASON_ZERO, + reportId, + resolveReport: true, + }); + + expect(res.status).toBe(409); + expect(await res.json()).toMatchObject({ error: "report_closed" }); + expect(h.warningRows()).toEqual([]); + expect(h.reportEvents(reportId)).toEqual(events); + expect(h.publish).not.toHaveBeenCalled(); + }); +}); + +describe("staff notes", () => { + it("shows them only with can_view_notes or to the moderator who wrote them", async () => { + const h = await harness(); + await h.issued(SENIOR, { + overrideNote: OVERRIDE_NOTE, + points: 1, + staffNote: STAFF_NOTE, + }); + await h.issued(BASIC, { staffNote: "basic-note" }); + + const notesOf = (list: StaffWarning[]) => + list.map(warning => [warning.staffNote, warning.overrideNote]); + + expect(notesOf((await h.standing(SENIOR)).warnings)).toEqual([ + ["basic-note", null], + [STAFF_NOTE, OVERRIDE_NOTE], + ]); + expect(notesOf((await h.standing(VIEWER)).warnings)).toEqual([ + [null, null], + [null, null], + ]); + const listed = await h.call(VIEWER, "GET", `/warnings?userId=${MEMBER}`); + expect( + notesOf(((await listed.json()) as { items: StaffWarning[] }).items), + ).toEqual([ + [null, null], + [null, null], + ]); + const basic = await h.standing(BASIC); + expect(notesOf(basic.warnings)).toEqual([["basic-note", null]]); + expect(basic.hiddenWarnings).toBe(1); + }); + + it("never reach the member's own status, notifications, events, audit trail or moderation log", async () => { + const h = await harness(); + await h.seedPoints(MEMBER, 6); + const { sanction, warningId } = await h.issued(SENIOR, { + overrideNote: OVERRIDE_NOTE, + points: 3, + staffNote: STAFF_NOTE, + }); + await h.revoke(SENIOR, "warnings", warningId); + await h.revoke(SENIOR, "sanctions", sanction?.id ?? 0); + + const suspended = await h.call(0, "GET", "/account-status", undefined, { + "x-suspended": String(MEMBER), + }); + expect(suspended.status).toBe(200); + const status = await suspended.text(); + expect((await h.call(0, "GET", "/account-status")).status).toBe(401); + + expect(h.emit.mock.calls.map(([name]) => name)).toEqual([ + "moderation.warning.issued", + "moderation.sanction.applied", + "moderation.warning.revoked", + "moderation.sanction.revoked", + ]); + const log = await h.call(SENIOR, "GET", `/members/${MEMBER}/log`); + expect(log.status).toBe(200); + const trail = JSON.stringify([ + await log.text(), + h.memory.rows(core_audit_logs), + ]); + expect(trail).not.toContain("Issued by mistake"); + + const leaked = JSON.stringify([ + status, + h.emit.mock.calls, + h.publish.mock.calls.map(([args]) => args.data), + trail, + ]); + expect(leaked).not.toContain(STAFF_NOTE); + expect(leaked).not.toContain(OVERRIDE_NOTE); + }); +}); diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.test.ts b/packages/vitnode/src/api/modules/users/routes/session.route.test.ts index 452297d81..bc382479b 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.test.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.test.ts @@ -86,6 +86,9 @@ describe("GET /users/session", () => { }); it("answers with no user when signed out", async () => { - expect(await readSession(null)).toEqual({ user: null }); + expect(await readSession(null)).toEqual({ + suspension: null, + user: null, + }); }); }); diff --git a/packages/vitnode/src/api/modules/users/routes/session.route.ts b/packages/vitnode/src/api/modules/users/routes/session.route.ts index 7af52f1da..0675de041 100644 --- a/packages/vitnode/src/api/modules/users/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/session.route.ts @@ -41,6 +41,13 @@ export const sessionRoute = buildRoute({ notifications: zodNotificationState, }) .nullable(), + suspension: z + .object({ endsAt: z.date().nullable() }) + .nullable() + .optional() + .describe( + "Set while the session's account is suspended; `user` is then `null`", + ), }), }, }, @@ -50,7 +57,14 @@ export const sessionRoute = buildRoute({ }, handler: async c => { const user = c.get("user"); - if (!user) return c.json({ user: null }); + if (!user) { + const suspension = c.get("suspension"); + + return c.json({ + user: null, + suspension: suspension ? { endsAt: suspension.endsAt } : null, + }); + } const [{ isAdmin, isModerator }, notifications] = await Promise.all([ getStaffFlags(c, user), diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index c0809290d..25b63dd69 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -54,6 +54,19 @@ export const newBuildPluginApiCore = buildApiPlugin({ moderator: { users: ["can_edit", "can_view_hidden_presence"], widgets: ["can_edit"], + warnings: [ + "can_view", + "can_warn", + "can_override_points", + "can_override_expiration", + { permission: "can_view_notes", dependsOn: ["can_view"] }, + { permission: "can_revoke", dependsOn: ["can_view"] }, + "can_view_sanctions", + { + permission: "can_manage_sanctions", + dependsOn: ["can_view_sanctions"], + }, + ], }, admin: { dashboard: [ @@ -117,6 +130,10 @@ export const newBuildPluginApiCore = buildApiPlugin({ { permission: "can_edit", dependsOn: ["can_view"] }, { permission: "can_delete", dependsOn: ["can_view"] }, ], + warning_settings: [ + "can_view", + { permission: "can_manage", dependsOn: ["can_view"] }, + ], staff_moderators: [ "can_view", { permission: "can_create", dependsOn: ["can_view"] }, diff --git a/packages/vitnode/src/content/server/public-permissions.ts b/packages/vitnode/src/content/server/public-permissions.ts index e0e4ff144..98ddb76d3 100644 --- a/packages/vitnode/src/content/server/public-permissions.ts +++ b/packages/vitnode/src/content/server/public-permissions.ts @@ -8,6 +8,8 @@ import type { ContentId } from "../ids"; import type { AnyContentTypeDefinition } from "../types"; import type { ContentModel } from "./model"; +import { resolveStaffPermissions } from "../../api/lib/check-staff-permission"; +import { canWarnForContent } from "../../api/lib/moderation/warnings/access"; import { buildRoute } from "../../api/lib/route"; import { contentTypeName } from "../admin/labels"; import { CONTENT_PUBLIC_MAX_PAGE_SIZE } from "../const"; @@ -53,6 +55,13 @@ export interface ContentViewerPermissions { * to sign in. */ canReport: boolean; + /** + * A **Warn author** action makes sense: the type has `reports`, the + * visitor is a moderator with `can_warn` for the record's scope and did not + * write it. `POST /moderation/warnings` re-checks all of it, with the + * author resolved on the server. + */ + canWarn: boolean; } const parseIds = ( @@ -90,6 +99,25 @@ export const resolveContentViewerPermissions = async ( .from(table) .where(inArray(columns.id, ids))) as Record[]; const user = c.get("user"); + const moderatorSet = + user && definition.reports.enabled + ? await resolveStaffPermissions(c, { type: "moderator", user }) + : null; + const scopeColumn = model.moderationScope?.column; + const canWarnOn = (row: Record, ownerId: unknown) => { + if (!moderatorSet?.staff || typeof ownerId !== "number") return false; + if (ownerId === user?.id) return false; + const scope = scopeColumn ? row[scopeColumn] : null; + + return canWarnForContent(c, moderatorSet, { + pluginId, + scopeId: + typeof scope === "number" || typeof scope === "string" + ? String(scope) + : null, + targetType: definition.id, + }); + }; const decided = await Promise.all( rows.map(async row => { @@ -138,6 +166,7 @@ export const resolveContentViewerPermissions = async ( definition.reports.enabled && isPublic && (!isOwner || definition.reports.selfReport), + canWarn: definition.reports.enabled && canWarnOn(row, ownerId), id, }; }), @@ -176,7 +205,7 @@ export const contentPermissionsRoute = < route: { method: "get", path: "/permissions", - description: `What the current visitor may do with each listed ${name}: edit, delete, hide or report it`, + description: `What the current visitor may do with each listed ${name}: edit, delete, hide, report it or warn its author`, request: { query: z.object({ ids: z @@ -196,6 +225,7 @@ export const contentPermissionsRoute = < canEdit: z.boolean(), canHide: z.boolean(), canReport: z.boolean(), + canWarn: z.boolean(), id: schemas.params.shape.id, }), ), diff --git a/packages/vitnode/src/content/server/report-target.ts b/packages/vitnode/src/content/server/report-target.ts index 23064ad96..d03cb0284 100644 --- a/packages/vitnode/src/content/server/report-target.ts +++ b/packages/vitnode/src/content/server/report-target.ts @@ -7,6 +7,7 @@ import type { ModerationCaptureResult, ModerationReportTargetDefinition, ModerationTargetActionDefinition, + ModerationTargetAuthors, ModerationTargetState, } from "../../api/lib/moderation/targets"; import type { @@ -410,6 +411,35 @@ const currentContent = async ( }; }; +const contentAuthors = async ( + c: Context, + model: AnyContentModel, + targetId: string, +): Promise => { + const definition = contentDefinitionOf(model); + const itemId = parseContentId(definition.idStrategy, targetId); + if (itemId === null) return null; + + const db = c.get("db"); + const base = await readBaseRow(db, model, itemId); + if (!base) return null; + + const titleField = definition.reports.titleField; + let rawTitle: unknown = titleField ? base[titleField] : null; + if (titleField && rawTitle === undefined && definition.localization.enabled) { + const translation = await readTranslationRow(c, db, model, { + itemId, + locale: definition.localization.defaultLocale, + }); + rawTitle = translation?.[titleField] ?? null; + } + + return { + authorIds: await contentAuthorIds(c, model, itemId), + title: reportTitleOf(rawTitle), + }; +}; + export const visibilityAction = ( model: AnyContentModel, pluginId: string, @@ -485,6 +515,8 @@ export const contentReportTarget = ( return id === null ? null : contentIdKey(id); }, permissionModule: definition.permissionModule, + resolveAuthors: async (c, { targetId }) => + await contentAuthors(c, model, targetId), ...(scope && scopeColumn ? { resolveScope: async (c: Context, targetId: string) => { diff --git a/packages/vitnode/src/database/warnings.ts b/packages/vitnode/src/database/warnings.ts new file mode 100644 index 000000000..a32d19532 --- /dev/null +++ b/packages/vitnode/src/database/warnings.ts @@ -0,0 +1,181 @@ +import { sql } from "drizzle-orm"; +import { camelCase, index, unique, uniqueIndex } from "drizzle-orm/pg-core"; + +import type { + SanctionSource, + SanctionThresholdSnapshot, + SanctionType, + WarningContentSnapshot, + WarningReasonSnapshot, + WarningThresholdAction, +} from "@/lib/moderation/warnings"; + +import { + SANCTION_SOURCES, + SANCTION_TYPES, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import { core_moderation_reports } from "./moderation"; +import { core_users } from "./users"; + +/** Warning reasons. Their title, description and default message live in `core_languages_words`. */ +export const core_moderation_warning_reasons = camelCase.table.withRLS( + "core_moderation_warning_reasons", + t => ({ + id: t.serial().primaryKey(), + points: t.integer().notNull().default(0), + /** `null` means the points never expire. */ + expiresAfterHours: t.integer(), + enabled: t.boolean().notNull().default(true), + position: t.integer().notNull().default(0), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }), + t => [index("core_moderation_warning_reasons_position_idx").on(t.position)], +); + +export const core_moderation_warning_thresholds = camelCase.table.withRLS( + "core_moderation_warning_thresholds", + t => ({ + id: t.serial().primaryKey(), + points: t.integer().notNull(), + action: t + .varchar({ enum: WARNING_THRESHOLD_ACTIONS, length: 32 }) + .$type() + .notNull(), + /** Length of a temporary suspension; `null` for an indefinite one. */ + durationHours: t.integer(), + enabled: t.boolean().notNull().default(true), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }), + t => [ + // Two enabled rules at the same points would be ambiguous. + uniqueIndex("core_moderation_warning_thresholds_enabled_points_idx") + .on(t.points) + .where(sql`"enabled" = true`), + ], +); + +export const core_moderation_warnings = camelCase.table.withRLS( + "core_moderation_warnings", + t => ({ + id: t.serial().primaryKey(), + userId: t + .integer() + .notNull() + .references(() => core_users.id, { onDelete: "cascade" }), + issuedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + reasonId: t.integer().references(() => core_moderation_warning_reasons.id, { + onDelete: "set null", + }), + reasonSnapshot: t.jsonb().$type(), + points: t.integer().notNull(), + /** The reason's points when the warning was issued, for auditing overrides. */ + defaultPoints: t.integer(), + /** Staff-only explanation of a points or expiration override. */ + overrideNote: t.text(), + /** Shown to the warned member. */ + message: t.text().notNull(), + /** Shown to staff with `can_view_notes` only. */ + staffNote: t.text(), + issuedAt: t.timestamp().notNull().defaultNow(), + /** When the points stop counting; `null` never. */ + expiresAt: t.timestamp(), + /** Active points just before and after this warning, as the server computed them. */ + pointsBefore: t.integer().notNull().default(0), + pointsAfter: t.integer().notNull().default(0), + pluginId: t.varchar({ length: 100 }), + targetType: t.varchar({ length: 100 }), + targetId: t.varchar({ length: 64 }), + scopeFamily: t.varchar({ length: 100 }), + scopeId: t.varchar({ length: 64 }), + contentSnapshot: t.jsonb().$type(), + reportId: t.integer().references(() => core_moderation_reports.id, { + onDelete: "set null", + }), + idempotencyKey: t.varchar({ length: 64 }), + revokedAt: t.timestamp(), + revokedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + revokeReason: t.text(), + }), + t => [ + unique("core_moderation_warnings_idempotency_key").on( + t.issuedById, + t.idempotencyKey, + ), + index("core_moderation_warnings_user_idx").on(t.userId, t.issuedAt), + index("core_moderation_warnings_issued_at_id_idx").on(t.issuedAt, t.id), + index("core_moderation_warnings_issued_by_idx").on(t.issuedById), + index("core_moderation_warnings_reason_idx").on(t.reasonId), + index("core_moderation_warnings_target_idx").on( + t.pluginId, + t.targetType, + t.targetId, + ), + index("core_moderation_warnings_report_idx").on(t.reportId), + ], +); + +export const core_moderation_sanctions = camelCase.table.withRLS( + "core_moderation_sanctions", + t => ({ + id: t.serial().primaryKey(), + userId: t + .integer() + .notNull() + .references(() => core_users.id, { onDelete: "cascade" }), + type: t + .varchar({ enum: SANCTION_TYPES, length: 32 }) + .$type() + .notNull(), + source: t + .varchar({ enum: SANCTION_SOURCES, length: 32 }) + .$type() + .notNull(), + startsAt: t.timestamp().notNull().defaultNow(), + /** `null` for an indefinite sanction. */ + endsAt: t.timestamp(), + /** The warning whose threshold evaluation created it - at most one per warning. */ + warningId: t + .integer() + .unique() + .references(() => core_moderation_warnings.id, { onDelete: "set null" }), + thresholdId: t + .integer() + .references(() => core_moderation_warning_thresholds.id, { + onDelete: "set null", + }), + thresholdSnapshot: t.jsonb().$type(), + createdById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + createdAt: t.timestamp().notNull().defaultNow(), + revokedAt: t.timestamp(), + revokedById: t.integer().references(() => core_users.id, { + onDelete: "set null", + }), + revokeReason: t.text(), + }), + t => [ + index("core_moderation_sanctions_user_idx").on( + t.userId, + t.revokedAt, + t.endsAt, + ), + index("core_moderation_sanctions_created_at_id_idx").on(t.createdAt, t.id), + ], +); diff --git a/packages/vitnode/src/lib/audit/actions.ts b/packages/vitnode/src/lib/audit/actions.ts index c9a066418..65e405d0a 100644 --- a/packages/vitnode/src/lib/audit/actions.ts +++ b/packages/vitnode/src/lib/audit/actions.ts @@ -135,6 +135,16 @@ export const CORE_AUDIT_ACTIONS = defineAuditActions([ { action: "report.resolved", targetType: "report" }, { action: "report.reopened", targetType: "report" }, { action: "report.action_taken", targetType: "report" }, + { action: "warning.issued", targetType: "user" }, + { action: "warning.revoked", targetType: "user" }, + { action: "sanction.applied", targetType: "user" }, + { action: "sanction.revoked", targetType: "user" }, + { action: "warning_reason.created", targetType: "warning_reason" }, + { action: "warning_reason.updated", targetType: "warning_reason" }, + { action: "warning_reason.reordered", targetType: "warning_reason" }, + { action: "warning_threshold.created", targetType: "warning_threshold" }, + { action: "warning_threshold.updated", targetType: "warning_threshold" }, + { action: "warning_threshold.deleted", targetType: "warning_threshold" }, { action: "submission.approved", targetType: "submission" }, { action: "submission.rejected", targetType: "submission" }, { action: "cron.run", targetType: "cron_job" }, diff --git a/packages/vitnode/src/lib/moderation/const.ts b/packages/vitnode/src/lib/moderation/const.ts index 2dabe6a6c..b8ed07940 100644 --- a/packages/vitnode/src/lib/moderation/const.ts +++ b/packages/vitnode/src/lib/moderation/const.ts @@ -5,6 +5,7 @@ export const MODERATION_PERMISSIONS = { deleteNotes: "can_delete_notes", viewHidden: "can_view_hidden", hide: "can_hide", + warn: "can_warn", } as const; export const MODERATION_DEFAULT_REPORT_REASONS = [ @@ -63,6 +64,7 @@ export const MODERATION_REPORT_EVENT_TYPES = [ "action", "resolved", "reopened", + "warning", ] as const; export type ModerationReportEventType = diff --git a/packages/vitnode/src/lib/moderation/warnings.test.ts b/packages/vitnode/src/lib/moderation/warnings.test.ts new file mode 100644 index 000000000..cae903da9 --- /dev/null +++ b/packages/vitnode/src/lib/moderation/warnings.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; + +import type { WarningThresholdRule } from "./warnings"; + +import { + activeWarningPoints, + evaluateWarningThresholds, + isSanctionActiveAt, + thresholdSuspensionEnd, +} from "./warnings"; + +const NOW = new Date("2026-10-28T12:00:00.000Z"); +const HOUR = 60 * 60 * 1000; +const at = (hours: number) => new Date(NOW.getTime() + hours * HOUR); + +const rule = ( + id: number, + points: number, + extra: Partial = {}, +): WarningThresholdRule => ({ + action: "suspend_temporary", + durationHours: 24, + enabled: true, + id, + points, + ...extra, +}); + +describe("activeWarningPoints", () => { + it("counts warnings that are neither expired nor revoked", () => { + expect( + activeWarningPoints( + [ + { expiresAt: null, points: 3, revokedAt: null }, + { expiresAt: at(1), points: 2, revokedAt: null }, + { expiresAt: null, points: 0, revokedAt: null }, + { expiresAt: NOW, points: 5, revokedAt: null }, + { expiresAt: at(-1), points: 7, revokedAt: null }, + { expiresAt: null, points: 11, revokedAt: at(-2) }, + { expiresAt: at(1), points: 13, revokedAt: at(-2) }, + ], + NOW, + ), + ).toBe(5); + }); +}); + +describe("evaluateWarningThresholds", () => { + const eight = rule(1, 8); + const ten = rule(2, 10, { + action: "suspend_indefinite", + durationHours: null, + }); + const twelveOff = rule(3, 12, { enabled: false }); + const rules = [ten, twelveOff, eight]; + + it.each([ + { after: 8, applied: eight, before: 6, crossed: [eight] }, + { after: 11, applied: ten, before: 6, crossed: [eight, ten] }, + { after: 9, applied: null, before: 8, crossed: [] }, + { after: 12, applied: null, before: 10, crossed: [] }, + { after: 7, applied: null, before: 6, crossed: [] }, + { after: 20, applied: null, before: 11, crossed: [] }, + ])( + "applies only the highest enabled rule crossed from $before to $after", + ({ after, applied, before, crossed }) => { + expect(evaluateWarningThresholds({ after, before, rules })).toEqual({ + applied, + crossed, + }); + }, + ); +}); + +describe("thresholdSuspensionEnd", () => { + it.each([ + ["after its duration", rule(1, 8, { durationHours: 24 }), at(24)], + [ + "never for an indefinite one", + rule(2, 10, { action: "suspend_indefinite", durationHours: null }), + null, + ], + ])("ends a suspension %s", (_name, threshold, end) => { + expect(thresholdSuspensionEnd(threshold, NOW)).toEqual(end); + }); +}); + +describe("isSanctionActiveAt", () => { + it.each([ + ["active and temporary", { endsAt: at(1), revokedAt: null }, true], + ["indefinite", { endsAt: null, revokedAt: null }, true], + ["revoked", { endsAt: null, revokedAt: at(-1) }, false], + ["ended", { endsAt: NOW, revokedAt: null }, false], + ["not started", { endsAt: null, revokedAt: null, startsAt: at(1) }, false], + ])("treats a %s sanction as active: %s", (_name, sanction, active) => { + expect(isSanctionActiveAt({ startsAt: at(-2), ...sanction }, NOW)).toBe( + active, + ); + }); +}); diff --git a/packages/vitnode/src/lib/moderation/warnings.ts b/packages/vitnode/src/lib/moderation/warnings.ts new file mode 100644 index 000000000..92da40eb2 --- /dev/null +++ b/packages/vitnode/src/lib/moderation/warnings.ts @@ -0,0 +1,165 @@ +import type { MultiLangValue } from "@/lib/helpers/multi-lang"; + +/** Moderator permissions of core's `warnings` module. */ +export const WARNING_PERMISSIONS = { + view: "can_view", + warn: "can_warn", + overridePoints: "can_override_points", + overrideExpiration: "can_override_expiration", + viewNotes: "can_view_notes", + revoke: "can_revoke", + viewSanctions: "can_view_sanctions", + manageSanctions: "can_manage_sanctions", +} as const; + +/** The core permission module holding {@link WARNING_PERMISSIONS}. */ +export const WARNING_PERMISSION_MODULE = "warnings"; + +export const WARNING_LIMITS = { + maxPoints: 100, + maxThresholdPoints: 1000, + maxReasons: 50, + maxThresholds: 20, + /** Five years, the longest expiration or suspension a form accepts. */ + maxDurationHours: 24 * 365 * 5, + messageMinLength: 3, + messageMaxLength: 2000, + noteMaxLength: 2000, + explanationMinLength: 3, + reasonTitleMaxLength: 100, + reasonDescriptionMaxLength: 255, + reasonMessageMaxLength: 2000, + pageSize: 25, +} as const; + +export const WARNING_STATUSES = ["active", "expired", "revoked"] as const; + +export type WarningStatus = (typeof WARNING_STATUSES)[number]; + +export const WARNING_THRESHOLD_ACTIONS = [ + "suspend_temporary", + "suspend_indefinite", +] as const; + +export type WarningThresholdAction = (typeof WARNING_THRESHOLD_ACTIONS)[number]; + +/** Kinds of account sanction. Only suspensions exist so far. */ +export const SANCTION_TYPES = ["suspension"] as const; + +export type SanctionType = (typeof SANCTION_TYPES)[number]; + +export const SANCTION_SOURCES = ["threshold", "manual"] as const; + +export type SanctionSource = (typeof SANCTION_SOURCES)[number]; + +/** What a warning remembers about its reason, so renaming the reason never rewrites history. */ +export interface WarningReasonSnapshot { + title: MultiLangValue; +} + +/** What a warning remembers about the content it was issued for. */ +export interface WarningContentSnapshot { + title: null | string; + url: null | string; +} + +/** What a threshold sanction remembers about the rule that created it. */ +export interface SanctionThresholdSnapshot { + action: WarningThresholdAction; + durationHours: null | number; + points: number; +} + +/** + * The status of a warning at `now`. Derived from timestamps on every read, so + * it is right whether or not a scheduled job has run. Pure. + */ +export const warningStatusAt = ( + warning: { expiresAt: Date | null; revokedAt: Date | null }, + now: Date, +): WarningStatus => { + if (warning.revokedAt) return "revoked"; + if (warning.expiresAt && warning.expiresAt.getTime() <= now.getTime()) { + return "expired"; + } + + return "active"; +}; + +/** + * The points a user holds at `now`: warnings that are not revoked and have + * not expired. Pure. + */ +export const activeWarningPoints = ( + warnings: readonly { + expiresAt: Date | null; + points: number; + revokedAt: Date | null; + }[], + now: Date, +): number => + warnings.reduce( + (total, warning) => + warningStatusAt(warning, now) === "active" + ? total + warning.points + : total, + 0, + ); + +export interface WarningThresholdRule { + action: WarningThresholdAction; + durationHours: null | number; + enabled: boolean; + id: number; + points: number; +} + +export interface ThresholdEvaluation { + /** The one rule to apply: the highest crossed one, or `null`. */ + applied: null | T; + /** Every enabled rule the change crossed, lowest first. */ + crossed: T[]; +} + +/** + * Which threshold rules a change of active points from `before` to `after` + * crosses - a rule at `points` is crossed when `before < points <= after` - + * and which one applies: only the highest, so one warning never creates two + * suspensions. A rule already at or below `before` was crossed earlier and is + * not applied again. Pure. + */ +export const evaluateWarningThresholds = ({ + after, + before, + rules, +}: { + after: number; + before: number; + rules: readonly T[]; +}): ThresholdEvaluation => { + const crossed = rules + .filter( + rule => rule.enabled && before < rule.points && rule.points <= after, + ) + .sort((a, b) => a.points - b.points || a.id - b.id); + + return { applied: crossed.at(-1) ?? null, crossed }; +}; + +/** When a suspension created by `rule` at `now` ends; `null` for indefinite. Pure. */ +export const thresholdSuspensionEnd = ( + rule: Pick, + now: Date, +): Date | null => + rule.action === "suspend_temporary" && rule.durationHours + ? new Date(now.getTime() + rule.durationHours * 60 * 60 * 1000) + : null; + +/** Whether a sanction applies at `now`: not revoked, started and not ended. Pure. */ +export const isSanctionActiveAt = ( + sanction: { endsAt: Date | null; revokedAt: Date | null; startsAt: Date }, + now: Date, +): boolean => + !sanction.revokedAt && + sanction.startsAt.getTime() <= now.getTime() && + (sanction.endsAt === null || sanction.endsAt.getTime() > now.getTime()); diff --git a/packages/vitnode/src/locales/api/en.json b/packages/vitnode/src/locales/api/en.json index 24a6aeaaf..3709f8f4d 100644 --- a/packages/vitnode/src/locales/api/en.json +++ b/packages/vitnode/src/locales/api/en.json @@ -55,6 +55,31 @@ "admin_message": { "label": "Messages from administrators", "description": "Announcements an administrator sends you directly." + }, + "warning_issued": { + "label": "Warnings from moderators", + "description": "When a moderator warns you. Always on, so you never miss one.", + "title": "You received a warning ({points, plural, one {# point} other {# points}})", + "title_reason": "You received a warning for {reason} ({points, plural, one {# point} other {# points}})" + }, + "warning_revoked": { + "label": "Revoked warnings", + "description": "When a moderator takes back a warning you received.", + "title": "A warning was revoked", + "body": "Its {points, plural, one {# point no longer counts} other {# points no longer count}} against your account." + }, + "account_suspended": { + "label": "Account suspensions", + "description": "When your account is suspended after reaching a warning limit.", + "title": "Your account is suspended", + "body_until": "You can use your account again after {endsAt} (UTC).", + "body_indefinite": "Your account is suspended until a moderator lifts it." + }, + "suspension_revoked": { + "label": "Lifted suspensions", + "description": "When a moderator lifts a suspension of your account.", + "title": "A suspension was lifted", + "body": "A moderator lifted a suspension of your account. Check your account status to see if anything else still applies." } }, "email": { diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index b37ddb54a..500bb9eec 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -877,7 +877,9 @@ "removed": "Cover image removed." } }, - "presence": "Online status" + "presence": "Online status", + "warn": "Warn", + "view_warnings": "Warnings ({points, plural, one {# point} other {# points}})" }, "files": { "title": "My Files", @@ -1163,7 +1165,8 @@ "security": "Security", "sso": "Connected accounts", "notifications": "Notifications", - "ai": "AI usage" + "ai": "AI usage", + "standing": "Account status" }, "overview": { "desc": "Your avatar and the details on your account.", @@ -1736,6 +1739,7 @@ "by_author": "Comment deleted by user", "by_moderator": "Comment removed by a moderator" }, + "warn": "Warn author", "live": { "new_comments": "New comments were posted.", "show_new": "Show new comments", @@ -2861,12 +2865,240 @@ "resolved": "closed it as {resolution}", "reopened": "reopened it", "note_edited": "edited a note", - "note_deleted": "deleted a note" + "note_deleted": "deleted a note", + "warning": "warned ({points, plural, one {# point} other {# points}})" }, "note_required": "Write something before adding the note.", "before": "Before", "after": "After" } + }, + "warnings": { + "title": "Warnings" + } + }, + "warn": { + "button": "Warn author", + "button_member": "Warn member", + "title": "Warn {name}", + "title_loading": "Warn member", + "desc": "The member gets your message in a notification. Points count across the whole site.", + "no_reasons": "No warning reasons are set up yet. An administrator can add them in the AdminCP.", + "cancel": "Cancel", + "submit": "Issue warning", + "points": "{count, plural, one {# point} other {# points}}", + "custom_reason": "Custom warning", + "deleted_user": "Deleted account", + "global": { + "title": "Points count everywhere", + "desc": "This warning is about content in one place, but its points count toward account-wide limits that can suspend the whole account." + }, + "consequences": { + "title": "What happens", + "current": "Active points now", + "warning": "This warning", + "projected": "Active points after", + "action": "Automatic action:", + "no_action": "None.", + "already_suspended": "This account is already suspended.", + "related": "{count, plural, one {# warning was} other {# warnings were}} already issued for this content.", + "advisory": "A preview. The server checks again when you send it, in case someone else warned this member in the meantime." + }, + "sanction": { + "indefinite": "Account suspended indefinitely.", + "for": "Account suspended for", + "until": "(until {endsAt}).", + "suspended_until": "Account suspended until {endsAt}." + }, + "form": { + "reason": "Reason", + "no_reason": "No reason (custom warning)", + "points": "Points", + "points_desc": "Starts at the reason's points. Changing them needs an explanation.", + "expiration": "Points expire", + "expiration_modes": { + "default": "As the reason says", + "never": "Never", + "hours": "After a number of hours", + "at": "On a date" + }, + "expiration_hours": "Expire after", + "hours_unit": "hours", + "expiration_at": "Expire on", + "override_note": "Why the change?", + "override_note_desc": "Staff only. Required when you change the points or the expiry.", + "override_note_required": "Explain why you changed the points or the expiry.", + "expiration_at_required": "Pick when the points expire.", + "message": "Message to the member", + "message_desc": "The member reads this in the notification and on their account status page.", + "staff_note": "Staff note", + "staff_note_desc": "Only staff who can read notes see this. The member never does.", + "resolve_report": "Also close the report as action taken", + "resolve_report_desc": "Closes the report in the same step. If either one fails, neither happens." + }, + "done": { + "title": "Warning issued", + "points": "{name} now has {after, plural, one {# active point} other {# active points}}.", + "suspended_until": "Their account is suspended until {endsAt}.", + "suspended_indefinitely": "Their account is suspended indefinitely.", + "report_resolved": "The report is closed.", + "differs": "Their points changed after you opened the form, so this differs from the preview." + }, + "errors": { + "title": "Could not issue the warning", + "unknown": "Something went wrong. Try again in a moment.", + "unauthenticated": "Your session has ended. Sign in again.", + "validation": "Check the fields and try again.", + "forbidden": "You can't warn this member here.", + "not_found": "That no longer exists.", + "already_revoked": "It was already revoked or lifted.", + "author_mismatch": "This member didn't write that content.", + "content_not_found": "That content is gone, so the warning can't point at it.", + "expiration_override_forbidden": "You can't change when points expire.", + "idempotency_conflict": "Reopen the form and try again.", + "invalid_expiration": "Pick an expiry in the future, at most five years away.", + "invalid_points": "Points must be a whole number from 0 to 100.", + "override_note_required": "Explain why you changed the points or the expiry.", + "points_override_forbidden": "You can't change the points.", + "protected": "Staff accounts can't be warned here.", + "reason_disabled": "That reason was just disabled. Pick another one.", + "reason_not_found": "That reason was removed. Pick another one.", + "report_closed": "The report is already closed. Warn without closing it.", + "report_mismatch": "The report is about different content.", + "self": "You can't warn yourself.", + "user_not_found": "That member no longer exists." + }, + "duration": { + "days": "{count, plural, one {# day} other {# days}}", + "hours": "{count, plural, one {# hour} other {# hours}}" + }, + "statuses": { + "active": "Active", + "expired": "Expired", + "revoked": "Revoked" + }, + "revoke": { + "button": "Revoke", + "title": "Revoke this warning?", + "desc": "Its points stop counting right away. The warning stays in the history, and a suspension it led to stays until you lift it separately.", + "reason": "Reason", + "reason_desc": "Kept with the warning for staff.", + "done": "Warning revoked", + "done_desc": "The member was notified." + }, + "list": { + "title": "Warnings", + "desc": "Every warning you can see, newest first.", + "status_label": "Filter by status", + "all": "All", + "reason_filter": "Reason", + "all_reasons": "All reasons", + "empty_title": "No warnings here", + "empty_desc": "Nothing matches these filters. Peaceful.", + "member": "Member", + "issued_by": "Issued by", + "expires": "Points expire", + "never": "Never", + "default_points": "Reason's points", + "content": "Content", + "untitled": "Untitled", + "report": "Report", + "report_link": "Report #{id}", + "message": "Message to the member", + "staff_only": "Staff only", + "staff_note": "Staff note", + "override_note": "Why the points or expiry changed", + "revoked": "Revoked by .", + "lifted": "Lifted by .", + "sanction_stays": "The suspension this warning led to was not lifted with it." + }, + "lookup": { + "title": "Find a member", + "label": "Member name", + "placeholder": "Search by name", + "results": "{count, plural, =0 {No members found} one {# member found} other {# members found}}", + "empty": "No members match that name.", + "forbidden": "You can't look up members." + }, + "member": { + "title": "{name}'s standing", + "breadcrumb": "Member", + "desc": "Active points, warnings and suspensions.", + "active_points": "Active points", + "status": "Status", + "suspended": "Suspended", + "in_good_standing": "In good standing", + "sanctions": "Suspensions", + "no_sanctions": "No suspensions.", + "warnings": "Warnings", + "no_warnings": "No warnings you can see.", + "hidden_warnings": "{count, plural, one {# more warning is} other {# more warnings are}} outside your moderation scope." + }, + "sanctions": { + "temporary": "Temporary suspension", + "indefinite": "Indefinite suspension", + "active": "Active", + "lifted": "Lifted", + "ended": "Ended", + "lift": "Lift", + "lift_title": "Lift this suspension?", + "lift_desc": "The account works again unless another suspension still applies.", + "lifted_done": "Suspension lifted", + "lifted_done_desc": "Any other active suspension still applies.", + "ends": "Ends", + "source": "Source", + "from_threshold": "Reached {points, plural, one {# point} other {# points}}", + "manual": "Applied by staff" + }, + "log": { + "title": "Moderation log", + "loading": "Loading the moderation log", + "error": "Could not load the moderation log.", + "empty": "Nothing has happened to this account yet.", + "load_more": "Show older", + "someone": "Someone", + "warning_issued": " warned them for {points, plural, one {# point} other {# points}} ({before} → {after} active)", + "warning_revoked": " revoked a warning of {points, plural, one {# point} other {# points}}", + "sanction_applied_until": "'s warning suspended the account until {endsAt} (the {threshold}-point threshold)", + "sanction_applied_indefinite": "'s warning suspended the account indefinitely (the {threshold}-point threshold)", + "sanction_revoked": " lifted a suspension", + "other": ": {action}", + "points_overridden": "Points changed from the reason's {points, plural, one {# point} other {# points}}", + "expiration_overridden": "Expiry changed", + "report": "From report #{id}" + } + }, + "account_status": { + "title": "Account status", + "desc": "Your warnings and whether anything restricts your account.", + "loading": "Loading your account status", + "signed_out": "Sign in to see your account status.", + "sign_in": "Sign in", + "suspended_title": "Your account is suspended", + "suspended_until": "You can use your account again after {endsAt}.", + "suspended_indefinite": "Your account stays suspended until a moderator lifts the suspension.", + "suspended_restrictions": "Until then you can browse like a visitor, but you can't post, comment or change anything on your account.", + "contact": "Think this is a mistake? Contact the site's administrators.", + "good_title": "Your account is in good standing", + "good_desc": "Nothing restricts your account.", + "points_title": "Active points", + "points_desc": "Points from warnings that haven't expired or been revoked. Reaching a limit can suspend your account.", + "warnings_title": "Your warnings", + "no_warnings": "You haven't received any warnings.", + "expires": "Points expire:", + "never": "Never", + "content": "About:", + "untitled": "Untitled", + "sanctions_title": "Suspensions", + "sanction_temporary": "Temporary suspension", + "sanction_indefinite": "Indefinite suspension", + "sanction_active": "Active", + "sanction_lifted": "Lifted", + "sanction_ended": "Ended", + "notice": { + "until": "Your account is suspended until {endsAt}.", + "indefinite": "Your account is suspended.", + "details": "See details" } } }, @@ -3046,6 +3278,24 @@ "succeeded": "Payment succeeded", "failed": "Payment failed", "expired": "Payment expired" + }, + "warning": { + "issued": "Warned member", + "revoked": "Revoked a warning" + }, + "sanction": { + "applied": "Suspended account", + "revoked": "Lifted a suspension" + }, + "warning_reason": { + "created": "Created warning reason", + "updated": "Updated warning reason", + "reordered": "Reordered warning reasons" + }, + "warning_threshold": { + "created": "Created warning threshold", + "updated": "Updated warning threshold", + "deleted": "Deleted warning threshold" } }, "fields": { @@ -3069,7 +3319,12 @@ "provider": "Provider", "reference": "Reference", "customerId": "Customer id", - "errorCode": "Error code" + "errorCode": "Error code", + "points": "Points", + "expiresAfterHours": "Points expire after (hours)", + "enabled": "Enabled", + "action": "Action", + "durationHours": "Duration (hours)" } } }, @@ -3246,7 +3501,9 @@ }, "moderation": { "title": "Moderation", - "report_reasons": "Report reasons" + "report_reasons": "Report reasons", + "warning_reasons": "Warning reasons", + "warning_thresholds": "Warning thresholds" }, "user_bar": { "home_page": "Home Page", @@ -3992,6 +4249,116 @@ "last_reason": "Keep at least one reason - members need something to pick.", "unknown": "Something went wrong. Try again in a moment." } + }, + "warning_reasons": { + "title": "Warning reasons", + "desc": "The reasons moderators pick when they warn someone, with their default points and expiry. Drag to change the order.", + "create": "Add reason", + "limit": "You can have up to {max} reasons. Disable one you no longer use.", + "columns": { + "reason": "Reason", + "points": "Points", + "expires": "Points expire", + "status": "Status" + }, + "never": "Never", + "enabled": "Enabled", + "disabled": "Disabled", + "edit": "Edit", + "empty_title": "No warning reasons yet", + "empty_desc": "Add one so moderators can warn with consistent points.", + "reorder": { + "success": "Order saved", + "success_desc": "Moderators see the reasons in the new order." + }, + "form": { + "create_title": "Add warning reason", + "edit_title": "Edit warning reason", + "sheet_desc": "Changes apply to new warnings only. Warnings already issued keep their points, expiry and title.", + "title": "Title", + "title_desc": "Short and specific, like “Spam” or “Harassment”.", + "description": "Description", + "description_desc": "Helps moderators pick the right reason. Members don't see it.", + "points": "Points", + "points_desc": "How many points a warning adds. 0 makes it an official warning that adds none.", + "points_unit": "points", + "expires": "Points expire after", + "expires_desc": "After this, the points stop counting. The warning stays in the history.", + "or": "or", + "never_expires": "Never expire", + "days_unit": "days", + "message": "Default message", + "message_desc": "Prefilled in the warn form. Moderators can edit it before sending.", + "enabled": "Enabled", + "enabled_desc": "Disabled reasons can't be picked for new warnings. Old warnings stay as they are.", + "submit_create": "Add reason", + "submit_edit": "Save changes", + "created": "Reason added", + "created_desc": "Moderators can pick “{reason}” when they warn someone.", + "updated": "Reason saved", + "updated_desc": "New warnings use the new settings.", + "error": "Could not save the reason", + "error_desc": "Check the fields and try again.", + "limit_reached": "You can have up to {max} reasons." + } + }, + "warning_thresholds": { + "title": "Warning thresholds", + "desc": "What happens automatically when a warning brings a member's active points up to a limit.", + "create": "Add threshold", + "limit": "You can have up to {max} thresholds.", + "columns": { + "points": "Active points", + "action": "Action", + "status": "Status" + }, + "at_points": "At {points, plural, one {# point} other {# points}}", + "suspend_for": "Suspend for", + "suspend_indefinitely": "Suspend indefinitely", + "enabled": "Enabled", + "disabled": "Disabled", + "edit": "Edit", + "actions_label": "Actions for the threshold at {points} points", + "empty_title": "No thresholds yet", + "empty_desc": "Without one, warnings never suspend anyone automatically.", + "delete": { + "button": "Delete", + "title": "Delete the threshold at {points} points?", + "desc": "New warnings won't trigger it anymore. Suspensions it already created stay until they end or are lifted.", + "error": "Could not delete the threshold", + "success": "Threshold deleted", + "success_desc": "Reaching {points} points no longer suspends anyone." + }, + "form": { + "create_title": "Add threshold", + "edit_title": "Edit threshold", + "sheet_desc": "When a warning brings active points from below this number to it or above, the action runs once.", + "future_only": "Changes apply to warnings issued from now on. Nobody is suspended or unsuspended just because you saved this.", + "points": "Active points", + "points_desc": "Only one enabled threshold can use each number.", + "points_unit": "points", + "action": "Action", + "actions": { + "suspend_temporary": "Temporary suspension", + "suspend_temporary_desc": "Ends on its own after the duration, even if the points are still there.", + "suspend_indefinite": "Indefinite suspension", + "suspend_indefinite_desc": "Lasts until a moderator lifts it." + }, + "duration": "Duration", + "duration_desc": "How long the suspension lasts. 24 is a day, 168 a week.", + "hours_unit": "hours", + "enabled": "Enabled", + "enabled_desc": "Disabled thresholds never run.", + "submit_create": "Add threshold", + "submit_edit": "Save changes", + "created": "Threshold added", + "updated": "Threshold saved", + "saved_desc": "It applies to the next warnings.", + "error": "Could not save the threshold", + "error_desc": "Check the fields and try again.", + "duplicate_points": "Another enabled threshold already uses this number of points.", + "limit_reached": "You can have up to {max} thresholds." + } } }, "system": { @@ -5048,5 +5415,18 @@ "description": "Rewrites the selected text: shorter, corrected, simpler or in another tone." } } - } + }, + "@vitnode/core:warnings": "Warnings", + "@vitnode/core:warnings:can_view": "View every member's warnings", + "@vitnode/core:warnings:can_warn": "Warn members directly, without content", + "@vitnode/core:warnings:can_override_points": "Change a warning's points", + "@vitnode/core:warnings:can_override_expiration": "Change when a warning's points expire", + "@vitnode/core:warnings:can_view_notes": "Read staff notes on warnings", + "@vitnode/core:warnings:can_revoke": "Revoke warnings", + "@vitnode/core:warnings:can_view_sanctions": "View suspensions", + "@vitnode/core:warnings:can_manage_sanctions": "Lift suspensions", + "@vitnode/core:warning_settings": "Warning settings", + "@vitnode/core:warning_settings:can_view": "View warning reasons and thresholds", + "@vitnode/core:warning_settings:can_manage": "Manage warning reasons and thresholds", + "@vitnode/core:moderation:can_warn": "Warn authors of this content" } diff --git a/packages/vitnode/src/pages/account-status.tsx b/packages/vitnode/src/pages/account-status.tsx new file mode 100644 index 000000000..530c8790b --- /dev/null +++ b/packages/vitnode/src/pages/account-status.tsx @@ -0,0 +1,22 @@ +import type { AccountStatusRouteData } from "../tanstack/account-status/route"; + +import { loadAccountStatusRoute } from "../tanstack/account-status/route"; +import { + AccountStatusBreadcrumb, + AccountStatusRouteContent, +} from "../tanstack/account-status/screen"; +import { defineRoute } from "../tanstack/plugin-routes"; + +const AccountStatusPage = () => ; + +// Open to every visitor on purpose: a suspended account is a guest to every +// other route, and this is where it learns why. +export const route = defineRoute({ + load: async ({ context, t }) => + await loadAccountStatusRoute({ ...context, t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: AccountStatusBreadcrumb, +}); + +export default AccountStatusPage; diff --git a/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx b/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx new file mode 100644 index 000000000..c3727a6d9 --- /dev/null +++ b/packages/vitnode/src/pages/admin/moderation/warning-reasons.tsx @@ -0,0 +1,25 @@ +import type { PluginRoutePageProps } from "@/routing"; +import type { AdminWarningsRouteData } from "@/tanstack/admin/warnings/route"; + +import { adminBreadcrumb } from "@/tanstack/admin/breadcrumb"; +import { loadAdminWarningReasonsRoute } from "@/tanstack/admin/warnings/route"; +import { AdminWarningReasonsRouteContent } from "@/tanstack/admin/warnings/screen"; +import { defineAdminRoute } from "@/tanstack/plugin-routes"; + +const AdminWarningReasonsPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAdminRoute({ + load: async ({ context, t }) => + await loadAdminWarningReasonsRoute({ ...context, t }), + head: ({ loaderData }) => ({ ...loaderData }), + + breadcrumb: adminBreadcrumb({ + segments: ["core", "moderation", "warning-reasons"], + }), +}); + +export default AdminWarningReasonsPage; diff --git a/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx b/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx new file mode 100644 index 000000000..eaeaa132a --- /dev/null +++ b/packages/vitnode/src/pages/admin/moderation/warning-thresholds.tsx @@ -0,0 +1,25 @@ +import type { PluginRoutePageProps } from "@/routing"; +import type { AdminWarningsRouteData } from "@/tanstack/admin/warnings/route"; + +import { adminBreadcrumb } from "@/tanstack/admin/breadcrumb"; +import { loadAdminWarningThresholdsRoute } from "@/tanstack/admin/warnings/route"; +import { AdminWarningThresholdsRouteContent } from "@/tanstack/admin/warnings/screen"; +import { defineAdminRoute } from "@/tanstack/plugin-routes"; + +const AdminWarningThresholdsPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAdminRoute({ + load: async ({ context, t }) => + await loadAdminWarningThresholdsRoute({ ...context, t }), + head: ({ loaderData }) => ({ ...loaderData }), + + breadcrumb: adminBreadcrumb({ + segments: ["core", "moderation", "warning-thresholds"], + }), +}); + +export default AdminWarningThresholdsPage; diff --git a/packages/vitnode/src/pages/modcp/member.tsx b/packages/vitnode/src/pages/modcp/member.tsx new file mode 100644 index 000000000..5672c9d14 --- /dev/null +++ b/packages/vitnode/src/pages/modcp/member.tsx @@ -0,0 +1,24 @@ +import type { PluginRoutePageProps } from "@/routing"; + +import type { ModcpMemberRouteData } from "../../tanstack/moderation/warnings-route"; + +import { ModcpMemberBreadcrumb } from "../../tanstack/moderation/breadcrumb"; +import { loadModcpMemberRoute } from "../../tanstack/moderation/warnings-route"; +import { ModcpMemberRouteContent } from "../../tanstack/moderation/warnings-screen"; +import { defineAuthenticatedRoute } from "../../tanstack/plugin-routes"; + +const ModcpMemberPage = ({ + loaderData, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAuthenticatedRoute({ + load: async ({ context, params, t }) => + await loadModcpMemberRoute({ ...context, id: params.id ?? "", t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: ModcpMemberBreadcrumb, +}); + +export default ModcpMemberPage; diff --git a/packages/vitnode/src/pages/modcp/warnings.tsx b/packages/vitnode/src/pages/modcp/warnings.tsx new file mode 100644 index 000000000..ec13779a4 --- /dev/null +++ b/packages/vitnode/src/pages/modcp/warnings.tsx @@ -0,0 +1,34 @@ +import type { PluginRoutePageProps } from "@/routing"; + +import type { ModcpRouteData } from "../../tanstack/moderation/route"; +import type { ModcpWarningsRouteSearch } from "../../tanstack/moderation/warnings-route"; + +import { ModcpWarningsBreadcrumb } from "../../tanstack/moderation/breadcrumb"; +import { loadModcpWarningsRoute } from "../../tanstack/moderation/warnings-route"; +import { ModcpWarningsRouteContent } from "../../tanstack/moderation/warnings-screen"; +import { defineAuthenticatedRoute } from "../../tanstack/plugin-routes"; + +const ModcpWarningsPage = ({ + loaderData, + navigate, + search, +}: PluginRoutePageProps) => ( + +); + +export const route = defineAuthenticatedRoute< + ModcpRouteData, + ModcpWarningsRouteSearch +>({ + load: async ({ context, search, t }) => + await loadModcpWarningsRoute({ ...context, search, t }), + head: ({ loaderData }) => ({ robots: "noindex, nofollow", ...loaderData }), + + breadcrumb: ModcpWarningsBreadcrumb, +}); + +export default ModcpWarningsPage; diff --git a/packages/vitnode/src/routes.tsx b/packages/vitnode/src/routes.tsx index 9c56c1f05..f0f877036 100644 --- a/packages/vitnode/src/routes.tsx +++ b/packages/vitnode/src/routes.tsx @@ -1,4 +1,5 @@ import { defineRoutes, index, layout, lazy, page } from "./routing"; +import { ACCOUNT_STATUS_NAMESPACES } from "./tanstack/account-status/route"; import { ADMIN_AI_NAMESPACES } from "./tanstack/admin/ai/route"; import { normalizeAiHistoryRouteSearch, @@ -41,6 +42,7 @@ import { normalizeStaffRouteSearch } from "./tanstack/admin/staff/route-search"; import { ADMIN_USER_NAMESPACES } from "./tanstack/admin/users/detail-route"; import { ADMIN_USERS_NAMESPACES } from "./tanstack/admin/users/route"; import { normalizeUsersRouteSearch } from "./tanstack/admin/users/route-search"; +import { ADMIN_WARNINGS_NAMESPACES } from "./tanstack/admin/warnings/route"; import { LOGIN_NAMESPACES } from "./tanstack/auth/login-route"; import { LoginPendingSkeleton, @@ -65,6 +67,7 @@ import { normalizeMyFilesRouteSearch } from "./tanstack/files/route-search"; import { normalizeModcpApprovalsSearch } from "./tanstack/moderation/approvals-route-search"; import { MODCP_NAMESPACES } from "./tanstack/moderation/namespaces"; import { normalizeModcpReportsSearch } from "./tanstack/moderation/route-search"; +import { normalizeModcpWarningsSearch } from "./tanstack/moderation/warnings-route"; import { NOTIFICATIONS_NAMESPACES } from "./tanstack/notifications/namespaces"; import { normalizeNotificationsRouteSearch } from "./tanstack/notifications/route-search"; import { @@ -237,9 +240,26 @@ export const routes = defineRoutes([ component: lazy(() => import("./pages/modcp/approval")), pendingComponent: () => , }), + + page("warnings", { + component: lazy(() => import("./pages/modcp/warnings")), + pendingComponent: () => , + search: normalizeModcpWarningsSearch, + }), + + page("members/:id", { + component: lazy(() => import("./pages/modcp/member")), + pendingComponent: () => , + }), ], }), + page("/account-status", { + component: lazy(() => import("./pages/account-status")), + messages: ACCOUNT_STATUS_NAMESPACES, + pendingComponent: () => , + }), + page("/notifications", { component: lazy(() => import("./pages/notifications")), messages: NOTIFICATIONS_NAMESPACES, @@ -371,6 +391,22 @@ export const routes = defineRoutes([ pendingComponent: TablePendingSkeleton, }), + page("/admin/core/moderation/warning-reasons", { + area: "admin", + component: lazy(() => import("./pages/admin/moderation/warning-reasons")), + messages: ADMIN_WARNINGS_NAMESPACES, + pendingComponent: TablePendingSkeleton, + }), + + page("/admin/core/moderation/warning-thresholds", { + area: "admin", + component: lazy( + () => import("./pages/admin/moderation/warning-thresholds"), + ), + messages: ADMIN_WARNINGS_NAMESPACES, + pendingComponent: TablePendingSkeleton, + }), + page("/admin/core/users", { area: "admin", component: lazy(() => import("./pages/admin/users/index")), diff --git a/packages/vitnode/src/tanstack/account-status/route.ts b/packages/vitnode/src/tanstack/account-status/route.ts new file mode 100644 index 000000000..d964163bf --- /dev/null +++ b/packages/vitnode/src/tanstack/account-status/route.ts @@ -0,0 +1,33 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import type { PluginRouteTranslator } from "@/routing"; + +import { accountStatusQueryOptions } from "@/views/moderation/warnings/warnings-api"; + +export const ACCOUNT_STATUS_NAMESPACES = [ + "core.global", + "core.moderation", +] as const; + +export interface AccountStatusRouteData { + description: string; + title: string; +} + +export const loadAccountStatusRoute = async ({ + queryClient, + t, +}: { + queryClient: QueryClient; + t: PluginRouteTranslator; +}): Promise => { + await queryClient.query({ + ...accountStatusQueryOptions(), + staleTime: "static", + }); + + return { + description: t("core.moderation.account_status.desc"), + title: t("core.moderation.account_status.title"), + }; +}; diff --git a/packages/vitnode/src/tanstack/account-status/screen.tsx b/packages/vitnode/src/tanstack/account-status/screen.tsx new file mode 100644 index 000000000..2700b3e7e --- /dev/null +++ b/packages/vitnode/src/tanstack/account-status/screen.tsx @@ -0,0 +1,18 @@ +import { useTranslations } from "use-intl"; + +import { AccountStatusContent } from "@/views/moderation/warnings/account-status-content"; + +import { RouteMessages } from "../i18n/route-messages"; +import { ACCOUNT_STATUS_NAMESPACES } from "./route"; + +export const AccountStatusBreadcrumb = () => { + const t = useTranslations("core.moderation.account_status"); + + return <>{t("title")}; +}; + +export const AccountStatusRouteContent = () => ( + + + +); diff --git a/packages/vitnode/src/tanstack/admin/warnings/route.ts b/packages/vitnode/src/tanstack/admin/warnings/route.ts new file mode 100644 index 000000000..eb2db6188 --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/warnings/route.ts @@ -0,0 +1,62 @@ +import type { PluginRouteTranslator } from "@/routing"; + +import { + adminWarningReasonsQueryOptions, + adminWarningThresholdsQueryOptions, +} from "@/views/admin/views/core/moderation/warnings/warnings-admin-api"; +import { ADMIN_WARNING_PERMISSIONS } from "@/views/admin/views/core/shared/admin-permissions"; + +import type { AdminScreenContext } from "../screen"; + +import { adminIdentityOf } from "../identity"; +import { requireAdminPermission } from "../screen"; + +export const ADMIN_WARNINGS_NAMESPACES = [ + "admin.moderation.warning_reasons", + "admin.moderation.warning_thresholds", + "core.global", + "core.moderation", +] as const; + +export interface AdminWarningsRouteData { + description: string; + title: string; +} + +export const loadAdminWarningReasonsRoute = async ({ + adminAccess, + queryClient, + t, +}: AdminScreenContext & { + t: PluginRouteTranslator; +}): Promise => { + requireAdminPermission(adminAccess, ADMIN_WARNING_PERMISSIONS.view); + await queryClient.query({ + ...adminWarningReasonsQueryOptions(adminIdentityOf(adminAccess)), + staleTime: "static", + }); + + return { + description: t("admin.moderation.warning_reasons.desc"), + title: t("admin.moderation.warning_reasons.title"), + }; +}; + +export const loadAdminWarningThresholdsRoute = async ({ + adminAccess, + queryClient, + t, +}: AdminScreenContext & { + t: PluginRouteTranslator; +}): Promise => { + requireAdminPermission(adminAccess, ADMIN_WARNING_PERMISSIONS.view); + await queryClient.query({ + ...adminWarningThresholdsQueryOptions(adminIdentityOf(adminAccess)), + staleTime: "static", + }); + + return { + description: t("admin.moderation.warning_thresholds.desc"), + title: t("admin.moderation.warning_thresholds.title"), + }; +}; diff --git a/packages/vitnode/src/tanstack/admin/warnings/screen.tsx b/packages/vitnode/src/tanstack/admin/warnings/screen.tsx new file mode 100644 index 000000000..82907f0ac --- /dev/null +++ b/packages/vitnode/src/tanstack/admin/warnings/screen.tsx @@ -0,0 +1,188 @@ +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import React from "react"; +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; + +import type { WarningReasonSheetState } from "@/views/admin/views/core/moderation/warnings/warning-reasons-content"; +import type { WarningThresholdSheetState } from "@/views/admin/views/core/moderation/warnings/warning-thresholds-content"; + +import { + AdminStaffPermissionGate, + useAdminStaffPermission, +} from "@/components/staff-permission/provider"; +import { PageTitle } from "@/components/ui/page-title"; +import { + CreateWarningReasonButton, + WarningReasonsContent, +} from "@/views/admin/views/core/moderation/warnings/warning-reasons-content"; +import { + CreateWarningThresholdButton, + WarningThresholdsContent, +} from "@/views/admin/views/core/moderation/warnings/warning-thresholds-content"; +import { + adminWarningReasonsQueryOptions, + adminWarningReasonsQueryRoot, + adminWarningThresholdsQueryOptions, + adminWarningThresholdsQueryRoot, + deleteAdminWarningThreshold, + reorderAdminWarningReasons, + saveAdminWarningReason, + saveAdminWarningThreshold, +} from "@/views/admin/views/core/moderation/warnings/warnings-admin-api"; +import { isAdminMutationError } from "@/views/admin/views/core/shared/admin-mutation"; +import { ADMIN_WARNING_PERMISSIONS } from "@/views/admin/views/core/shared/admin-permissions"; +import { WARNINGS_QUERY_ROOT } from "@/views/moderation/warnings/warnings-api"; + +import type { AdminWarningsRouteData } from "./route"; + +import { RouteMessages } from "../../i18n/route-messages"; +import { useAdminIdentity } from "../identity"; +import { ADMIN_WARNINGS_NAMESPACES } from "./route"; + +const WarningReasonsScreen = ({ + description, + title, +}: AdminWarningsRouteData) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const adminUserId = useAdminIdentity(); + const queryClient = useQueryClient(); + const { data } = useSuspenseQuery( + adminWarningReasonsQueryOptions(adminUserId), + ); + const canEdit = useAdminStaffPermission(ADMIN_WARNING_PERMISSIONS.manage); + const [sheet, setSheet] = React.useState({ + mode: "create", + }); + const [sheetOpen, setSheetOpen] = React.useState(false); + const refresh = async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: adminWarningReasonsQueryRoot(adminUserId), + }), + queryClient.invalidateQueries({ queryKey: WARNINGS_QUERY_ROOT }), + ]); + }; + + return ( +
+ + + { + setSheet({ mode: "create" }); + setSheetOpen(true); + }} + /> + + + + { + setSheet(state); + setSheetOpen(true); + }} + onReorder={async ids => { + const result = await reorderAdminWarningReasons(ids); + if (isAdminMutationError(result)) { + throw new Error( + `Saving the order answered ${result.error.status}.`, + ); + } + await refresh(); + toast.success(t("reorder.success"), { + description: t("reorder.success_desc"), + }); + }} + onSave={async args => { + const result = await saveAdminWarningReason(args); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onSheetOpenChange={setSheetOpen} + reasons={data} + sheet={sheet} + sheetOpen={sheetOpen} + /> +
+ ); +}; + +export const AdminWarningReasonsRouteContent = ( + props: AdminWarningsRouteData, +) => ( + + + +); + +const WarningThresholdsScreen = ({ + description, + title, +}: AdminWarningsRouteData) => { + const adminUserId = useAdminIdentity(); + const queryClient = useQueryClient(); + const { data } = useSuspenseQuery( + adminWarningThresholdsQueryOptions(adminUserId), + ); + const canEdit = useAdminStaffPermission(ADMIN_WARNING_PERMISSIONS.manage); + const [sheet, setSheet] = React.useState({ + mode: "create", + }); + const [sheetOpen, setSheetOpen] = React.useState(false); + const refresh = async () => { + await queryClient.invalidateQueries({ + queryKey: adminWarningThresholdsQueryRoot(adminUserId), + }); + }; + + return ( +
+ + + { + setSheet({ mode: "create" }); + setSheetOpen(true); + }} + /> + + + + { + const result = await deleteAdminWarningThreshold(id); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onOpenSheet={state => { + setSheet(state); + setSheetOpen(true); + }} + onSave={async args => { + const result = await saveAdminWarningThreshold(args); + if (!isAdminMutationError(result)) await refresh(); + + return result; + }} + onSheetOpenChange={setSheetOpen} + sheet={sheet} + sheetOpen={sheetOpen} + thresholds={data} + /> +
+ ); +}; + +export const AdminWarningThresholdsRouteContent = ( + props: AdminWarningsRouteData, +) => ( + + + +); diff --git a/packages/vitnode/src/tanstack/comments/author.ts b/packages/vitnode/src/tanstack/comments/author.ts new file mode 100644 index 000000000..0db6795df --- /dev/null +++ b/packages/vitnode/src/tanstack/comments/author.ts @@ -0,0 +1,21 @@ +import type { ContentPublicPerson } from "@/content/public-person"; + +import { zodContentPublicPerson } from "@/content/public-person"; + +const zodAuthor = zodContentPublicPerson.nullable().catch(null); + +/** + * The comment's author from its public record's `author` field, or `null` + * for a deleted account or a content type without one. Pure. + */ +export const contentCommentAuthor = ( + record: unknown, +): ContentPublicPerson | null => + typeof record === "object" && record !== null && "author" in record + ? zodAuthor.parse(record.author) + : null; + +/** The name to show for a person, preferring a real name they share. Pure. */ +export const contentCommentAuthorName = ( + person: ContentPublicPerson | null, +): null | string => (person ? (person.fullName ?? person.name) : null); diff --git a/packages/vitnode/src/tanstack/comments/item.tsx b/packages/vitnode/src/tanstack/comments/item.tsx index c29def897..cf7b13b1e 100644 --- a/packages/vitnode/src/tanstack/comments/item.tsx +++ b/packages/vitnode/src/tanstack/comments/item.tsx @@ -6,13 +6,12 @@ import { FlagIcon, PencilIcon, Trash2Icon, + TriangleAlertIcon, } from "lucide-react"; import React from "react"; import { toast } from "sonner"; import { useTranslations } from "use-intl"; -import type { ContentPublicPerson } from "@/content/public-person"; - import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -29,13 +28,13 @@ import { PopoverTitle, } from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; -import { zodContentPublicPerson } from "@/content/public-person"; import type { ContentCommentComposerFormProps } from "./composer-form"; import type { ContentCommentHideFormProps } from "./hide-form"; import type { ContentCommentNode } from "./thread"; import type { ContentCommentEditable } from "./use-content-comments"; +import { contentCommentAuthor, contentCommentAuthorName } from "./author"; import { useContentCommentErrorToast, useContentCommentsContext } from "./root"; const loadComposer = async () => await import("./composer-form"); @@ -75,23 +74,7 @@ const LazyHideForm = (props: ContentCommentHideFormProps) => ( ); -const zodAuthor = zodContentPublicPerson.nullable().catch(null); - -/** - * The comment's author from its public record's `author` field, or `null` - * for a deleted account or a content type without one. Pure. - */ -export const contentCommentAuthor = ( - record: unknown, -): ContentPublicPerson | null => - typeof record === "object" && record !== null && "author" in record - ? zodAuthor.parse(record.author) - : null; - -/** The name to show for a person, preferring a real name they share. Pure. */ -export const contentCommentAuthorName = ( - person: ContentPublicPerson | null, -): null | string => (person ? (person.fullName ?? person.name) : null); +export { contentCommentAuthor, contentCommentAuthorName } from "./author"; /** * The state one comment needs: what the visitor may do with it, whether an @@ -142,6 +125,10 @@ export const useContentCommentItem = (node: ContentCommentNode) => { canEdit: comments.features.edit && permissions.canEdit, canHide: comments.features.hide && permissions.canHide, canReport: comments.features.report && permissions.canReport, + canWarn: + comments.features.warn && + permissions.canWarn && + contentCommentAuthor(node.record) !== null, }, setIsReplying, /** Loads the editable values, then opens the edit form. */ @@ -174,7 +161,7 @@ const HIDE_PROMPT_MORPH_CLASS = "[clip-path:inset(-2rem_round_var(--radius-lg))] transition-[opacity,scale,clip-path] duration-200 data-starting-style:scale-100 data-starting-style:[clip-path:inset(0_calc(100%-var(--menu-width,100%))_calc(100%-var(--menu-height,100%))_0_round_var(--radius-lg))] data-[side=top]:data-starting-style:[clip-path:inset(calc(100%-var(--menu-height,100%))_calc(100%-var(--menu-width,100%))_0_0_round_var(--radius-lg))]"; /** - * The **More actions** menu of one comment: Edit, Report, Hide or Unhide and + * The **More actions** menu of one comment: Edit, Report, Warn author, Hide or Unhide and * Delete, each shown only when the plugin wired it up and the server allows * it. Hiding asks for an optional reason in a popover; deleting asks for * confirmation. Renders nothing when no action is available. @@ -194,7 +181,7 @@ export const ContentCommentActions = ({ node: ContentCommentNode; }) => { const t = useTranslations("core.comments"); - const { comments, requestDelete, requestReport } = + const { comments, requestDelete, requestReport, requestWarn } = useContentCommentsContext(); const showError = useContentCommentErrorToast(); const menuRef = React.useRef(null); @@ -202,13 +189,16 @@ export const ContentCommentActions = ({ const [isHidePromptOpen, setIsHidePromptOpen] = React.useState(false); const [hidePromptOrigin, setHidePromptOrigin] = React.useState(null); - const { canDelete, canEdit, canHide, canReport } = item.permissions; + const { canDelete, canEdit, canHide, canReport, canWarn } = item.permissions; const isHidden = node.hidden; const name = contentCommentAuthorName(contentCommentAuthor(node.record)) ?? t("deleted_user"); - if (!node.record || (!canEdit && !canReport && !canHide && !canDelete)) { + if ( + !node.record || + (!canEdit && !canReport && !canWarn && !canHide && !canDelete) + ) { return children ? ( ) : null} - {(canHide || canDelete) && (canEdit || canReport || children) ? ( + {canWarn ? ( + { + requestWarn(node); + }} + > + + ) : null} + {(canHide || canDelete) && + (canEdit || canReport || canWarn || children) ? ( ) : null} {canHide ? ( diff --git a/packages/vitnode/src/tanstack/comments/root.tsx b/packages/vitnode/src/tanstack/comments/root.tsx index 33a5aa873..9bb82af20 100644 --- a/packages/vitnode/src/tanstack/comments/root.tsx +++ b/packages/vitnode/src/tanstack/comments/root.tsx @@ -17,6 +17,8 @@ import { useLocale } from "../i18n/locale"; import { intlQueryOptions } from "../i18n/query"; import { getIntlRuntime } from "../i18n/runtime"; import { ReportDialog } from "../moderation/report-button"; +import { WarnDialog } from "../moderation/warn-button"; +import { contentCommentAuthor } from "./author"; /** The message namespaces the shared comments components render with. */ export const CONTENT_COMMENTS_NAMESPACES = [ @@ -30,6 +32,8 @@ interface ContentCommentsContextValue { requestDelete: (node: ContentCommentNode) => void; /** Opens the report dialog for one comment. */ requestReport: (node: ContentCommentNode) => void; + /** Opens the warn dialog for one comment's author. */ + requestWarn: (node: ContentCommentNode) => void; } /** @@ -108,17 +112,23 @@ const RootDialogs = ({ deleting, onDeleteClosed, onReportClosed, + onWarnClosed, reporting, + warning, }: { deleting: ContentCommentNode | null; onDeleteClosed: () => void; onReportClosed: () => void; + onWarnClosed: () => void; reporting: ContentCommentNode | null; + warning: ContentCommentNode | null; }) => { const t = useTranslations("core.comments"); const { comments } = useContentCommentsContext(); const showError = useContentCommentErrorToast(); const target = reporting ? comments.reportTarget(reporting.id) : null; + const warnTarget = warning ? comments.reportTarget(warning.id) : null; + const warnAuthor = warning ? contentCommentAuthor(warning.record) : null; return ( <> @@ -162,14 +172,25 @@ const RootDialogs = ({ typeLabel={t("report_type")} /> ) : null} + + {warnTarget && warnAuthor ? ( + { + if (!open) onWarnClosed(); + }} + open + userId={warnAuthor.id} + /> + ) : null} ); }; /** * Provides one comment thread to the shared comment components below it and - * renders the dialogs they open: the delete confirmation and the report - * dialog. Adds its own messages to the page's, so it works on any page and + * renders the dialogs they open: the delete confirmation, the report + * dialog and the warn dialog. Adds its own messages to the page's, so it works on any page and * your components inside keep their translations. Use it to lay out * comments your own way; `ContentComments` renders a complete section with * it. @@ -204,6 +225,8 @@ export const ContentCommentsRoot = ({ React.useState | null>(null); const [reporting, setReporting] = React.useState | null>(null); + const [warning, setWarning] = + React.useState | null>(null); const value = React.useMemo( () => ({ @@ -211,6 +234,7 @@ export const ContentCommentsRoot = ({ comments: comments as unknown as ContentCommentsController, requestDelete: setDeleting, requestReport: setReporting, + requestWarn: setWarning, }), [comments], ); @@ -228,7 +252,11 @@ export const ContentCommentsRoot = ({ onReportClosed={() => { setReporting(null); }} + onWarnClosed={() => { + setWarning(null); + }} reporting={reporting} + warning={warning} /> diff --git a/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx b/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx index 78ca94350..71f2134c5 100644 --- a/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx +++ b/packages/vitnode/src/tanstack/comments/use-content-comments.test.tsx @@ -154,6 +154,7 @@ const NONE = { canEdit: false, canHide: false, canReport: false, + canWarn: false, }; type Comments = ReturnType< diff --git a/packages/vitnode/src/tanstack/comments/use-content-comments.ts b/packages/vitnode/src/tanstack/comments/use-content-comments.ts index ab509fffa..6daf45d55 100644 --- a/packages/vitnode/src/tanstack/comments/use-content-comments.ts +++ b/packages/vitnode/src/tanstack/comments/use-content-comments.ts @@ -346,6 +346,8 @@ export interface ContentCommentsController< hide: boolean; replies: boolean; report: boolean; + /** Warning authors reuses `report`'s target, so it is on whenever `report` is. */ + warn: boolean; }; /** The rich-text field that holds a comment's text. */ field: string; @@ -795,6 +797,7 @@ export const useContentComments = < hide: Boolean(visibility), replies: maxDepth !== undefined, report: Boolean(report), + warn: Boolean(report), }, field, hasMore: comments.hasNextPage, diff --git a/packages/vitnode/src/tanstack/content/permissions.test.tsx b/packages/vitnode/src/tanstack/content/permissions.test.tsx index 53a7db1c8..4cccee504 100644 --- a/packages/vitnode/src/tanstack/content/permissions.test.tsx +++ b/packages/vitnode/src/tanstack/content/permissions.test.tsx @@ -84,6 +84,7 @@ describe("useContentPermissions", () => { canEdit: false, canHide: false, canReport: false, + canWarn: false, }); for (let settled = 0; settled < 5; settled++) { @@ -106,10 +107,34 @@ describe("useContentPermissions", () => { ).toStrictEqual(ids); expect(result.current.isError).toBe(false); expect([2, 3, 228, 229].map(id => result.current.of(id))).toStrictEqual([ - { canDelete: false, canEdit: true, canHide: false, canReport: true }, - { canDelete: false, canEdit: false, canHide: false, canReport: true }, - { canDelete: false, canEdit: true, canHide: false, canReport: true }, - { canDelete: false, canEdit: false, canHide: false, canReport: true }, + { + canDelete: false, + canEdit: true, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: false, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: true, + canHide: false, + canReport: true, + canWarn: false, + }, + { + canDelete: false, + canEdit: false, + canHide: false, + canReport: true, + canWarn: false, + }, ]); }); @@ -137,12 +162,14 @@ describe("useContentPermissions", () => { canEdit: false, canHide: false, canReport: false, + canWarn: false, }); expect(result.current.of(answered)).toStrictEqual({ canDelete: false, canEdit: true, canHide: false, canReport: true, + canWarn: false, }); }); }); diff --git a/packages/vitnode/src/tanstack/content/permissions.ts b/packages/vitnode/src/tanstack/content/permissions.ts index 81304b7f4..dbea1c7be 100644 --- a/packages/vitnode/src/tanstack/content/permissions.ts +++ b/packages/vitnode/src/tanstack/content/permissions.ts @@ -26,11 +26,19 @@ export interface ContentPermissions { * the visitor's - unless `reports.selfReport` lets authors report their own. */ canReport: boolean; + /** + * A **Warn author** action makes sense: the visitor is a moderator with + * `can_warn` for the record's scope and did not write it. + */ + canWarn: boolean; } /** The permissions route's answer, as `fetch` returns it. */ export interface ContentPermissionsResponse { - items: (ContentPermissions & { id: TId })[]; + items: (Omit & { + canWarn?: boolean; + id: TId; + })[]; } const NO_PERMISSIONS: ContentPermissions = { @@ -38,6 +46,7 @@ const NO_PERMISSIONS: ContentPermissions = { canEdit: false, canHide: false, canReport: false, + canWarn: false, }; /** Batches in flight at once for one hook. */ @@ -194,6 +203,7 @@ export const useContentPermissions = ({ canEdit: item.canEdit, canHide: item.canHide, canReport: item.canReport, + canWarn: item.canWarn ?? false, } : NO_PERMISSIONS; }, diff --git a/packages/vitnode/src/tanstack/layout/main-header.tsx b/packages/vitnode/src/tanstack/layout/main-header.tsx index f9bed610e..bcea09f74 100644 --- a/packages/vitnode/src/tanstack/layout/main-header.tsx +++ b/packages/vitnode/src/tanstack/layout/main-header.tsx @@ -1,7 +1,16 @@ +import { SuspensionNotice } from "../moderation/suspension-notice"; import { Header } from "./header"; import { UserHeader } from "./user-header"; -/** The site `Header` with the signed-in user's menu - what the main layout renders. */ +/** + * The site `Header` with the signed-in user's menu - what the main layout + * renders - followed by the suspension notice while the account is suspended. + */ export const MainHeader = ( props: Omit, "user">, -) =>
} />; +) => ( + <> +
} /> + + +); diff --git a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx index 2d28b347f..5dc70417e 100644 --- a/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx +++ b/packages/vitnode/src/tanstack/moderation/breadcrumb.tsx @@ -37,3 +37,19 @@ export const ModcpApprovalBreadcrumb = ({ return <>{t("title", { id: loaderData?.id ?? 0 })}; }; + +export const ModcpWarningsBreadcrumb = () => { + const t = useTranslations("core.moderation.warn.list"); + + return <>{t("title")}; +}; + +export const ModcpMemberBreadcrumb = ({ + loaderData, +}: { + loaderData?: { title: string }; +}) => { + const t = useTranslations("core.moderation.warn.member"); + + return <>{loaderData?.title ?? t("breadcrumb")}; +}; diff --git a/packages/vitnode/src/tanstack/moderation/index.ts b/packages/vitnode/src/tanstack/moderation/index.ts index 5be0fd279..e8e1734b9 100644 --- a/packages/vitnode/src/tanstack/moderation/index.ts +++ b/packages/vitnode/src/tanstack/moderation/index.ts @@ -6,4 +6,10 @@ export { ReportDialog, type ReportDialogProps, } from "./report-button"; +export { + WarnAuthorButton, + type WarnAuthorButtonProps, + WarnDialog, + type WarnDialogProps, +} from "./warn-button"; export type { ReportPreview } from "@/views/moderation/report/report-dialog-content"; diff --git a/packages/vitnode/src/tanstack/moderation/route.ts b/packages/vitnode/src/tanstack/moderation/route.ts index 5c386391d..f5d00a7a0 100644 --- a/packages/vitnode/src/tanstack/moderation/route.ts +++ b/packages/vitnode/src/tanstack/moderation/route.ts @@ -39,7 +39,7 @@ export const requireModeratorRoute = (auth: AuthenticatedState): void => { }; /** A serial id from the URL, or the route's 404. */ -const routeIdOf = (raw: string): number => { +export const routeIdOf = (raw: string): number => { const id = /^[1-9]\d{0,9}$/.test(raw) ? Number(raw) : Number.NaN; if (!Number.isSafeInteger(id) || id > 2_147_483_647) { // oxlint-disable-next-line typescript/only-throw-error @@ -50,7 +50,9 @@ const routeIdOf = (raw: string): number => { }; /** Turns the API's 404 - which also covers "not in your scope" - into the route's. */ -const withNotFound = async (load: () => Promise): Promise => { +export const withNotFound = async ( + load: () => Promise, +): Promise => { try { await load(); } catch (error) { diff --git a/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx b/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx new file mode 100644 index 000000000..9a0fac909 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/suspension-notice.tsx @@ -0,0 +1,61 @@ +import { Link } from "@tanstack/react-router"; +import { ShieldAlertIcon } from "lucide-react"; +import { useFormatter, useTranslations } from "use-intl"; + +import { useSessionQuery } from "../auth/session-query"; +import { RouteMessages } from "../i18n/route-messages"; +import { REPORT_NAMESPACES } from "./report-button"; + +export const ACCOUNT_STATUS_PATH = "/account-status"; + +const Notice = ({ endsAt }: { endsAt: Date | null | string }) => { + const t = useTranslations("core.moderation.account_status.notice"); + const format = useFormatter(); + + return ( +
+
+
+
+ ); +}; + +/** + * A strip under the header while the session's account is suspended. The API + * treats such a session as a guest everywhere; this tells the member why and + * links to their account status. Renders nothing otherwise. + */ +export const SuspensionNotice = () => { + const { data } = useSessionQuery(); + const suspension = data?.suspension; + if (!suspension) return null; + + return ( + + + + ); +}; diff --git a/packages/vitnode/src/tanstack/moderation/warn-button.tsx b/packages/vitnode/src/tanstack/moderation/warn-button.tsx new file mode 100644 index 000000000..e98c0b295 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warn-button.tsx @@ -0,0 +1,155 @@ +import { TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useTranslations } from "use-intl"; + +import type { ContentTarget } from "@/content/target"; +import type { IssuedWarning } from "@/views/moderation/warnings/warnings-api"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Skeleton } from "@/components/ui/skeleton"; + +import { RouteMessages } from "../i18n/route-messages"; +import { REPORT_NAMESPACES } from "./report-button"; + +const WarnDialogContent = React.lazy( + async () => await import("@/views/moderation/warnings/warn-dialog-content"), +); + +export interface WarnDialogProps { + /** + * The content the warning is for. The server resolves its authors and + * moderation scope itself and refuses the warning unless `userId` is one of + * the authors and the moderator has `can_warn` there. Leave it out for a + * direct warning, which needs the core `warnings` `can_warn` permission. + */ + content?: ContentTarget | null; + /** Called with the server's result once the warning is issued. */ + onIssued?: (result: IssuedWarning) => void; + onOpenChange: (open: boolean) => void; + open: boolean; + /** The report the warning answers, for the ModCP report screen. */ + reportId?: null | number; + /** The member to warn. */ + userId: number; +} + +const WarnDialogInner = ({ onOpenChange, open, ...props }: WarnDialogProps) => ( + + + {open ? ( + + + + + + } + > + { + onOpenChange(false); + }} + /> + + ) : null} + + +); + +/** + * The warn dialog on its own, for a menu item or any trigger you already + * have. Shows the reason, points, expiry, the member's message, a staff note + * and a live preview of the consequences; the form loads lazily. + * + * @example + * ```tsx + * import { WarnDialog } from "@vitnode/core/tanstack/moderation"; + * + * ; + * ``` + */ +export const WarnDialog = (props: WarnDialogProps) => ( + + + +); + +export interface WarnAuthorButtonProps extends Omit< + WarnDialogProps, + "onOpenChange" | "open" +> { + className?: string; + /** The id of the element naming the author, read after the button's label. */ + describedBy?: string; +} + +/** + * A **Warn author** button that opens {@link WarnDialog}. Render it only when + * the server allowed it - for a content type, when the permissions route + * answers `canWarn` - and the API checks everything again anyway. + * + * @example + * ```tsx + * import { contentTarget } from "@vitnode/core/content"; + * import { WarnAuthorButton } from "@vitnode/core/tanstack/moderation"; + * + * {permissions.canWarn ? ( + * + * ) : null} + * ``` + */ +export const WarnAuthorButton = ({ + className, + describedBy, + ...props +}: WarnAuthorButtonProps) => { + const [open, setOpen] = React.useState(false); + + return ( + + { + setOpen(true); + }} + /> + + + ); +}; + +const WarnAuthorButtonLabel = ({ + className, + describedBy, + onClick, +}: { + className?: string; + describedBy?: string; + onClick: () => void; +}) => { + const t = useTranslations("core.moderation.warn"); + + return ( + + ); +}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-route.ts b/packages/vitnode/src/tanstack/moderation/warnings-route.ts new file mode 100644 index 000000000..84c51e997 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warnings-route.ts @@ -0,0 +1,154 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import { notFound } from "@tanstack/react-router"; + +import type { PluginRouteTranslator } from "@/routing"; +import type { ModcpWarningsFilter } from "@/views/moderation/warnings/warnings-api"; + +import { asSearchValue, readPageSize } from "@/lib/table-params"; +import { ModcpRequestError } from "@/views/moderation/modcp/modcp-query"; +import { + memberStandingQueryOptions, + warningsListQueryOptions, +} from "@/views/moderation/warnings/warnings-api"; + +import type { AuthenticatedState } from "../auth/state"; +import type { ModcpRouteData } from "./route"; + +import { requireModeratorRoute, routeIdOf, withNotFound } from "./route"; + +export const MODCP_WARNINGS_PAGE_SIZE = 25; +const MAX_PAGE = 10_000; +const MAX_ID = 2_147_483_647; +const STATUSES = ["active", "expired", "revoked"] as const; + +export interface ModcpWarningsRouteSearch { + first?: number; + page?: number; + reason?: number; + /** `all` is what the URL says by saying nothing. */ + status?: (typeof STATUSES)[number]; + user?: number; +} + +const positiveInt = (raw: string | undefined, max: number) => { + if (raw === undefined || !/^\d+$/.test(raw)) return undefined; + const value = Number(raw); + + return Number.isSafeInteger(value) && value > 0 && value <= max + ? value + : undefined; +}; + +export const normalizeModcpWarningsSearch = ( + input: Record, +): ModcpWarningsRouteSearch => { + const status = asSearchValue(input.status); + const reason = positiveInt(asSearchValue(input.reason), MAX_ID); + const user = positiveInt(asSearchValue(input.user), MAX_ID); + const page = positiveInt(asSearchValue(input.page), MAX_PAGE); + const rawFirst = asSearchValue(input.first); + const first = + rawFirst === undefined ? undefined : readPageSize(rawFirst, 100); + + return { + ...(STATUSES.find(value => value === status) + ? { status: status as (typeof STATUSES)[number] } + : {}), + ...(reason === undefined ? {} : { reason }), + ...(user === undefined ? {} : { user }), + ...(page === undefined || page === 1 ? {} : { page }), + ...(first === undefined || Number(first) === MODCP_WARNINGS_PAGE_SIZE + ? {} + : { first: Number(first) }), + }; +}; + +export const modcpWarningsFilter = ( + search: ModcpWarningsRouteSearch, +): ModcpWarningsFilter => ({ + page: search.page ?? 1, + pageSize: search.first ?? MODCP_WARNINGS_PAGE_SIZE, + status: search.status ?? "all", + ...(search.reason === undefined ? {} : { reasonId: search.reason }), + ...(search.user === undefined ? {} : { userId: search.user }), +}); + +export const modcpWarningsSearchParams = ( + search: ModcpWarningsRouteSearch, +): URLSearchParams => + new URLSearchParams( + Object.entries(search) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => [key, String(value)]), + ); + +export const modcpWarningsSearchFrom = ( + query: string, +): ModcpWarningsRouteSearch => + normalizeModcpWarningsSearch(Object.fromEntries(new URLSearchParams(query))); + +export const loadModcpWarningsRoute = async ({ + auth, + queryClient, + search, + t, +}: { + auth: AuthenticatedState; + queryClient: QueryClient; + search: ModcpWarningsRouteSearch; + t: PluginRouteTranslator; +}): Promise => { + requireModeratorRoute(auth); + await queryClient.query({ + ...warningsListQueryOptions(modcpWarningsFilter(search)), + staleTime: "static", + }); + + return { + description: t("core.moderation.warn.list.desc"), + title: t("core.moderation.warn.list.title"), + }; +}; + +export interface ModcpMemberRouteData extends ModcpRouteData { + id: number; +} + +export const loadModcpMemberRoute = async ({ + auth, + id: raw, + queryClient, + t, +}: { + auth: AuthenticatedState; + id: string; + queryClient: QueryClient; + t: PluginRouteTranslator; +}): Promise => { + requireModeratorRoute(auth); + const id = routeIdOf(raw); + let name = ""; + try { + await withNotFound(async () => { + const standing = await queryClient.query({ + ...memberStandingQueryOptions(id), + staleTime: "static", + }); + name = standing.user.name; + }); + } catch (error) { + // A moderator who may neither warn nor view warnings gets the same 404. + if (error instanceof ModcpRequestError && error.status === 403) { + // oxlint-disable-next-line typescript/only-throw-error + throw notFound(); + } + throw error; + } + + return { + description: t("core.moderation.warn.member.desc"), + id, + title: t("core.moderation.warn.member.title", { name }), + }; +}; diff --git a/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx new file mode 100644 index 000000000..f662c3a58 --- /dev/null +++ b/packages/vitnode/src/tanstack/moderation/warnings-screen.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +import type { DataTableNavigation } from "@/components/table/navigation"; + +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { ModcpMemberContent } from "@/views/moderation/warnings/modcp-member-content"; +import { ModcpWarningsContent } from "@/views/moderation/warnings/modcp-warnings-content"; + +import type { ModcpRouteData } from "./route"; +import type { ModcpWarningsRouteSearch } from "./warnings-route"; + +import { RouteMessages } from "../i18n/route-messages"; +import { MODCP_NAMESPACES } from "./namespaces"; +import { + modcpWarningsFilter, + modcpWarningsSearchFrom, + modcpWarningsSearchParams, +} from "./warnings-route"; + +export const ModcpWarningsRouteContent = ({ + description, + navigate, + search, + title, +}: ModcpRouteData & { + navigate: (options: { + resetScroll?: boolean; + search: ModcpWarningsRouteSearch; + }) => Promise; + search: ModcpWarningsRouteSearch; +}) => { + const navigation = React.useMemo( + () => ({ + navigate: async nextSearch => { + await navigate({ + resetScroll: false, + search: modcpWarningsSearchFrom(nextSearch), + }); + }, + searchParams: modcpWarningsSearchParams(search), + }), + [navigate, search], + ); + + return ( + + + + + + ); +}; + +export const ModcpMemberRouteContent = ({ id }: { id: number }) => ( + + + +); diff --git a/packages/vitnode/src/tanstack/profile/screen.tsx b/packages/vitnode/src/tanstack/profile/screen.tsx index 641990f12..f49c0aebc 100644 --- a/packages/vitnode/src/tanstack/profile/screen.tsx +++ b/packages/vitnode/src/tanstack/profile/screen.tsx @@ -6,13 +6,14 @@ import { } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { cn } from "cn"; -import { UserRoundPenIcon } from "lucide-react"; +import { ShieldIcon, TriangleAlertIcon, UserRoundPenIcon } from "lucide-react"; import React from "react"; import { useTranslations } from "use-intl"; -import { buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { UserPresenceStatus } from "@/components/user-presence-status"; import { USER_HEADER_HREF } from "@/views/layouts/theme/header/user/user-header-model"; +import { memberStandingQueryOptions } from "@/views/moderation/warnings/warnings-api"; import { userPresenceQueryOptions } from "@/views/presence/presence-query"; import { ProfileContent } from "@/views/profile/profile-content"; import { userProfileQueryKey } from "@/views/profile/profile-query"; @@ -22,6 +23,7 @@ import { SearchFeedList } from "@/views/search/search-feed-content"; import { useSessionQuery } from "../auth/session-query"; import { useLocale } from "../i18n/locale"; import { RouteMessages } from "../i18n/route-messages"; +import { WarnDialog } from "../moderation/warn-button"; import { useOwnUserImageEditor } from "./own-image-editor"; import { userProfileQuery } from "./query"; import { PROFILE_NAMESPACES } from "./route"; @@ -72,6 +74,43 @@ const ProfilePresence = ({ ); }; +/** + * Warn and View warnings for moderators. Shown only when the server's + * standing route answers for this member - it decides who may warn directly. + */ +const ModeratorActions = ({ userId }: { userId: number }) => { + const t = useTranslations("core.profile"); + const [open, setOpen] = React.useState(false); + const { data } = useQuery(memberStandingQueryOptions(userId)); + if (!data) return null; + + return ( +
+ +
+ ); +}; + const ProfileTimeline = ({ userId, viewerId, @@ -116,7 +155,13 @@ const ProfileScreen = ({ children, nameCode }: ProfileRouteProps) => { return ( : null} + action={ + isOwner ? ( + + ) : session?.user?.isModerator ? ( + + ) : null + } editor={editor} presence={ ({ href: "/admin/core/moderation/report-reasons", permission: core("report_reasons"), }, + { + title: key("admin.global.nav.moderation.warning_reasons"), + href: "/admin/core/moderation/warning-reasons", + permission: core("warning_settings"), + }, + { + title: key("admin.global.nav.moderation.warning_thresholds"), + href: "/admin/core/moderation/warning-thresholds", + permission: core("warning_settings"), + }, ], }, { diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx new file mode 100644 index 000000000..63d64f6bf --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reason-form.tsx @@ -0,0 +1,205 @@ +import { toast } from "sonner"; +import { useLocale, useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormSheetFooter } from "@/components/form/auto-form-sheet-footer"; +import { AutoFormInput } from "@/components/form/fields/input"; +import { AutoFormNullableNumber } from "@/components/form/fields/nullable-number"; +import { AutoFormNumber } from "@/components/form/fields/number"; +import { AutoFormSwitch } from "@/components/form/fields/switch"; +import { AutoFormTextarea } from "@/components/form/fields/textarea"; +import { multiLangValueSchema } from "@/lib/helpers/multi-lang"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { resolveReasonText } from "@/views/moderation/report-reasons-query"; + +import type { + AdminWarningReason, + AdminWarningReasonInput, +} from "./warnings-admin-api"; + +export interface WarningReasonFormProps { + onSave: (args: { + id?: number; + values: AdminWarningReasonInput; + }) => Promise>; + onSaved: () => void; + reason?: AdminWarningReason; +} + +export const WarningReasonForm = ({ + onSave, + onSaved, + reason, +}: WarningReasonFormProps) => { + const t = useTranslations("admin.moderation.warning_reasons.form"); + const locale = useLocale(); + + const formSchema = z.object({ + title: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonTitleMaxLength, + minLength: 1, + }) + .min(1) + .default(reason?.title ?? []), + description: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonDescriptionMaxLength, + }).default(reason?.description ?? []), + points: z + .number() + .int() + .min(0) + .max(WARNING_LIMITS.maxPoints) + .default(reason?.points ?? 1), + expiresAfterDays: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours / 24) + .nullable() + .default( + reason + ? reason.expiresAfterHours === null + ? null + : Math.max(1, Math.round(reason.expiresAfterHours / 24)) + : 30, + ), + message: multiLangValueSchema({ + maxLength: WARNING_LIMITS.reasonMessageMaxLength, + }).default(reason?.message ?? []), + enabled: z.boolean().default(reason?.enabled ?? true), + }); + + const onSubmit: AutoFormOnSubmit = async ({ + expiresAfterDays, + ...values + }) => { + const result = await onSave({ + id: reason?.id, + values: { + ...values, + expiresAfterHours: + expiresAfterDays === null ? null : expiresAfterDays * 24, + }, + }); + + if ("error" in result) { + toast.error(t("error"), { + description: + result.error.status === 409 + ? t("limit_reached", { max: WARNING_LIMITS.maxReasons }) + : t("error_desc"), + }); + + return; + } + + toast.success(reason ? t("updated") : t("created"), { + description: reason + ? t("updated_desc") + : t("created_desc", { + reason: resolveReasonText(values.title, locale), + }), + }); + onSaved(); + }; + + return ( + ( + + ), + }, + { + id: "description", + component: props => ( + + ), + }, + { + id: "points", + component: props => ( + + ), + }, + { + id: "expiresAfterDays", + component: props => ( + + ), + }, + { + id: "message", + component: props => ( + + ), + }, + { + id: "enabled", + component: props => ( + + ), + }, + ]} + formSchema={formSchema} + id={reason ? `warning-reason-${reason.id}` : "warning-reason-new"} + layout={rendered => ( + <> +
+ {rendered.title} + {rendered.description} + {rendered.points} + {rendered.expiresAfterDays} + {rendered.message} + {rendered.enabled} +
+ + + )} + onSubmit={onSubmit} + /> + ); +}; + +export default WarningReasonForm; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx new file mode 100644 index 000000000..9d4dcad23 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-reasons-content.tsx @@ -0,0 +1,216 @@ +import { PencilIcon, PlusIcon, TriangleAlertIcon } from "lucide-react"; +import React from "react"; +import { useLocale, useTranslations } from "use-intl"; + +import type { DataTableNavigation } from "@/components/table/navigation"; + +import { ContentDataTable } from "@/components/table/content"; +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Spinner } from "@/components/ui/spinner"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { resolveReasonText } from "@/views/moderation/report-reasons-query"; +import { WarningDuration } from "@/views/moderation/warnings/warning-format"; + +import type { WarningReasonFormProps } from "./warning-reason-form"; +import type { AdminWarningReason } from "./warnings-admin-api"; + +const WarningReasonForm = React.lazy( + async () => await import("./warning-reason-form"), +); + +export type WarningReasonSheetState = + | { mode: "create" } + | { mode: "edit"; reason: AdminWarningReason }; + +const STATIC_NAVIGATION: DataTableNavigation = { + navigate: () => undefined, + searchParams: new URLSearchParams(), +}; + +export const CreateWarningReasonButton = ({ + count, + onCreate, +}: { + count: number; + onCreate: () => void; +}) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const atLimit = count >= WARNING_LIMITS.maxReasons; + + return ( + + ); +}; + +export const WarningReasonsContent = ({ + canEdit, + onOpenSheet, + onReorder, + onSave, + onSheetOpenChange, + reasons, + sheet, + sheetOpen, +}: { + canEdit: boolean; + onOpenSheet: (state: WarningReasonSheetState) => void; + onReorder: (ids: number[]) => Promise; + onSave: WarningReasonFormProps["onSave"]; + onSheetOpenChange: (open: boolean) => void; + reasons: AdminWarningReason[]; + sheet: WarningReasonSheetState; + sheetOpen: boolean; +}) => { + const t = useTranslations("admin.moderation.warning_reasons"); + const locale = useLocale(); + + return ( + <> + + + columns={[ + { + accessorKey: "title", + cell: ({ row }) => { + const description = resolveReasonText(row.description, locale); + + return ( + + + {resolveReasonText(row.title, locale)} + + {description ? ( + + {description} + + ) : null} + + ); + }, + header: t("columns.reason"), + }, + { + accessorKey: "points", + cell: ({ row }) => ( + {row.points} + ), + className: "w-24", + header: t("columns.points"), + }, + { + accessorKey: "expiresAfterHours", + cell: ({ row }) => + row.expiresAfterHours === null ? ( + + {t("never")} + + ) : ( + + ), + className: "w-32", + header: t("columns.expires"), + }, + { + accessorKey: "enabled", + cell: ({ row }) => + row.enabled ? ( + {t("enabled")} + ) : ( + {t("disabled")} + ), + className: "w-28", + header: t("columns.status"), + }, + { + align: "right", + cell: ({ row }) => { + const title = resolveReasonText(row.title, locale); + + return canEdit ? ( + + + + ) : null; + }, + className: "w-16", + header: "", + id: "actions", + }, + ]} + customNoResults={{ + description: t("empty_desc"), + icon: , + title: t("empty_title"), + }} + edges={reasons} + id="warning-reasons-table" + order={{ defaultOrder: { column: "position", order: "asc" } }} + reorderable={{ + disabled: !canEdit, + getRowLabel: row => resolveReasonText(row.title, locale), + onReorder, + }} + /> + + + + + + + {sheet.mode === "edit" + ? t("form.edit_title") + : t("form.create_title")} + + + {t("form.sheet_desc")} + + + + + + } + > + { + onSheetOpenChange(false); + }} + reason={sheet.mode === "edit" ? sheet.reason : undefined} + /> + + + + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx new file mode 100644 index 000000000..86b731568 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-threshold-form.tsx @@ -0,0 +1,166 @@ +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; +import { z } from "zod"; + +import type { AutoFormOnSubmit } from "@/components/form/auto-form"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { AutoForm } from "@/components/form/auto-form"; +import { AutoFormSheetFooter } from "@/components/form/auto-form-sheet-footer"; +import { AutoFormNumber } from "@/components/form/fields/number"; +import { AutoFormRadioGroup } from "@/components/form/fields/radio-group"; +import { AutoFormSwitch } from "@/components/form/fields/switch"; +import { + WARNING_LIMITS, + WARNING_THRESHOLD_ACTIONS, +} from "@/lib/moderation/warnings"; + +import type { + AdminWarningThreshold, + AdminWarningThresholdInput, +} from "./warnings-admin-api"; + +export interface WarningThresholdFormProps { + onSave: (args: { + id?: number; + values: AdminWarningThresholdInput; + }) => Promise>; + onSaved: () => void; + threshold?: AdminWarningThreshold; +} + +export const WarningThresholdForm = ({ + onSave, + onSaved, + threshold, +}: WarningThresholdFormProps) => { + const t = useTranslations("admin.moderation.warning_thresholds.form"); + + const formSchema = z.object({ + points: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxThresholdPoints) + .default(threshold?.points ?? 10), + action: z + .enum(WARNING_THRESHOLD_ACTIONS) + .default(threshold?.action ?? "suspend_temporary"), + durationHours: z + .number() + .int() + .min(1) + .max(WARNING_LIMITS.maxDurationHours) + .default(threshold?.durationHours ?? 24), + enabled: z.boolean().default(threshold?.enabled ?? true), + }); + + const onSubmit: AutoFormOnSubmit = async values => { + const result = await onSave({ + id: threshold?.id, + values: { + ...values, + durationHours: + values.action === "suspend_temporary" ? values.durationHours : null, + }, + }); + + if ("error" in result) { + toast.error(t("error"), { + description: + result.error.status === 409 + ? result.error.message?.includes("limit_reached") + ? t("limit_reached", { max: WARNING_LIMITS.maxThresholds }) + : t("duplicate_points") + : t("error_desc"), + }); + + return; + } + + toast.success(threshold ? t("updated") : t("created"), { + description: t("saved_desc"), + }); + onSaved(); + }; + + return ( + ( + + ), + }, + { + id: "action", + component: props => ( + ({ + description: t(`actions.${action}_desc`), + label: t(`actions.${action}`), + value: action, + }))} + /> + ), + }, + { + id: "durationHours", + hidden: values => values.action !== "suspend_temporary", + component: props => ( + + ), + }, + { + id: "enabled", + component: props => ( + + ), + }, + ]} + formSchema={formSchema} + id={ + threshold + ? `warning-threshold-${threshold.id}` + : "warning-threshold-new" + } + layout={rendered => ( + <> +
+

+ {t("future_only")} +

+ {rendered.points} + {rendered.action} + {rendered.durationHours} + {rendered.enabled} +
+ + + )} + onSubmit={onSubmit} + /> + ); +}; + +export default WarningThresholdForm; diff --git a/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx new file mode 100644 index 000000000..33fb9e1fe --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/moderation/warnings/warning-thresholds-content.tsx @@ -0,0 +1,241 @@ +import { GaugeIcon, PencilIcon, PlusIcon, Trash2Icon } from "lucide-react"; +import React from "react"; +import { toast } from "sonner"; +import { useTranslations } from "use-intl"; + +import type { DataTableNavigation } from "@/components/table/navigation"; +import type { AdminMutationResult } from "@/views/admin/views/core/shared/admin-mutation"; + +import { ConfirmActionAlertDialog } from "@/components/confirm-action/confirm-action-alert-dialog"; +import { ContentDataTable } from "@/components/table/content"; +import { DataTableNavigationProvider } from "@/components/table/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Spinner } from "@/components/ui/spinner"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { WARNING_LIMITS } from "@/lib/moderation/warnings"; +import { WarningDuration } from "@/views/moderation/warnings/warning-format"; + +import type { WarningThresholdFormProps } from "./warning-threshold-form"; +import type { AdminWarningThreshold } from "./warnings-admin-api"; + +const WarningThresholdForm = React.lazy( + async () => await import("./warning-threshold-form"), +); + +export type WarningThresholdSheetState = + | { mode: "create" } + | { mode: "edit"; threshold: AdminWarningThreshold }; + +const STATIC_NAVIGATION: DataTableNavigation = { + navigate: () => undefined, + searchParams: new URLSearchParams(), +}; + +export const CreateWarningThresholdButton = ({ + count, + onCreate, +}: { + count: number; + onCreate: () => void; +}) => { + const t = useTranslations("admin.moderation.warning_thresholds"); + const atLimit = count >= WARNING_LIMITS.maxThresholds; + + return ( + + ); +}; + +export const WarningThresholdsContent = ({ + canEdit, + onDelete, + onOpenSheet, + onSave, + onSheetOpenChange, + sheet, + sheetOpen, + thresholds, +}: { + canEdit: boolean; + onDelete: (id: number) => Promise>; + onOpenSheet: (state: WarningThresholdSheetState) => void; + onSave: WarningThresholdFormProps["onSave"]; + onSheetOpenChange: (open: boolean) => void; + sheet: WarningThresholdSheetState; + sheetOpen: boolean; + thresholds: AdminWarningThreshold[]; +}) => { + const t = useTranslations("admin.moderation.warning_thresholds"); + const [deleting, setDeleting] = React.useState( + null, + ); + + return ( + <> + + + columns={[ + { + accessorKey: "points", + cell: ({ row }) => ( + + {t("at_points", { points: row.points })} + + ), + header: t("columns.points"), + }, + { + accessorKey: "action", + cell: ({ row }) => + row.action === "suspend_temporary" && row.durationHours ? ( + + {t("suspend_for")}{" "} + + + ) : ( + {t("suspend_indefinitely")} + ), + header: t("columns.action"), + }, + { + accessorKey: "enabled", + cell: ({ row }) => + row.enabled ? ( + {t("enabled")} + ) : ( + {t("disabled")} + ), + className: "w-28", + header: t("columns.status"), + }, + { + align: "right", + cell: ({ row }) => + canEdit ? ( +
+ + + + + + +
+ ) : null, + className: "w-24", + header: "", + id: "actions", + }, + ]} + customNoResults={{ + description: t("empty_desc"), + icon: , + title: t("empty_title"), + }} + edges={thresholds} + id="warning-thresholds-table" + order={{ defaultOrder: { column: "points", order: "asc" } }} + /> +
+ + + + + + {sheet.mode === "edit" + ? t("form.edit_title") + : t("form.create_title")} + + + {t("form.sheet_desc")} + + + + + + } + > + { + onSheetOpenChange(false); + }} + threshold={sheet.mode === "edit" ? sheet.threshold : undefined} + /> + + + + + } + onOpenChange={open => { + if (!open) setDeleting(null); + }} + onSubmit={async ({ onClose }) => { + if (!deleting) return; + const result = await onDelete(deleting.id); + if ("error" in result) { + toast.error(t("delete.error")); + + return; + } + toast.success(t("delete.success"), { + description: t("delete.success_desc", { points: deleting.points }), + }); + onClose(); + }} + open={deleting !== null} + submitVariant="destructive" + textSubmit={ + <> +