From 9522cefdc126640c335f8c6718f5abae17ebcecd Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Thu, 5 Mar 2026 20:20:49 +0100 Subject: [PATCH] feat: pinned topics with category/forum-wide scope (#142) * feat(db): add pinnedAt and pinnedScope columns to topics * feat(api): expose isPinned, isLocked, pinnedScope in topic responses Add isPinned, isLocked, pinnedScope, and pinnedAt fields to both topicResponseProperties (JSON schema) and serializeTopic() so the frontend receives pinned/locked state in API responses. * feat(api): add scope support to pin endpoint (category/forum) * feat(api): sort pinned topics first in topic list queries Add CASE-based ORDER BY to GET /api/topics so pinned topics float to the top. When a category filter is active, both category-pinned and forum-pinned topics are promoted. On the homepage (no category filter), only forum-pinned topics get promoted. Secondary sort remains the user's chosen order (latest or popular). * fix(test): add pinned_at and pinned_scope columns to tenant isolation test schema --- drizzle/0009_brainy_sunspot.sql | 3 + drizzle/meta/0009_snapshot.json | 4066 ++++++++++++++++++++ drizzle/meta/_journal.json | 9 +- src/db/schema/topics.ts | 14 +- src/routes/moderation.ts | 28 +- src/routes/topics.ts | 22 +- src/validation/moderation.ts | 1 + tests/integration/tenant-isolation.test.ts | 2 + tests/unit/db/schema/topics.test.ts | 12 + tests/unit/routes/moderation.test.ts | 73 + tests/unit/routes/topics.test.ts | 118 + 11 files changed, 4342 insertions(+), 6 deletions(-) create mode 100644 drizzle/0009_brainy_sunspot.sql create mode 100644 drizzle/meta/0009_snapshot.json diff --git a/drizzle/0009_brainy_sunspot.sql b/drizzle/0009_brainy_sunspot.sql new file mode 100644 index 0000000..5a89123 --- /dev/null +++ b/drizzle/0009_brainy_sunspot.sql @@ -0,0 +1,3 @@ +ALTER TABLE "topics" ADD COLUMN "pinned_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "topics" ADD COLUMN "pinned_scope" text;--> statement-breakpoint +CREATE INDEX "topics_pinned_scope_idx" ON "topics" USING btree ("pinned_scope"); \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..9d553e9 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,4066 @@ +{ + "id": "2e645169-605f-48d4-8fae-e20020f0d918", + "prevId": "d168c26e-76b0-40aa-b1b3-81eeba8a99c8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reputation_score": { + "name": "reputation_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "declared_age": { + "name": "declared_age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "maturity_pref": { + "name": "maturity_pref", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'safe'" + }, + "account_created_at": { + "name": "account_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "followers_count": { + "name": "followers_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "follows_count": { + "name": "follows_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "atproto_posts_count": { + "name": "atproto_posts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_bluesky_profile": { + "name": "has_bluesky_profile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "atproto_labels": { + "name": "atproto_labels", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "users_role_elevated_idx": { + "name": "users_role_elevated_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "role IN ('moderator', 'admin')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_handle_idx": { + "name": "users_handle_idx", + "columns": [ + { + "expression": "handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_account_created_at_idx": { + "name": "users_account_created_at_idx", + "columns": [ + { + "expression": "account_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.firehose_cursor": { + "name": "firehose_cursor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'default'" + }, + "cursor": { + "name": "cursor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.topics": { + "name": "topics", + "schema": "", + "columns": { + "uri": { + "name": "uri", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_format": { + "name": "content_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "labels": { + "name": "labels", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reaction_count": { + "name": "reaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vote_count": { + "name": "vote_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pinned_scope": { + "name": "pinned_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_mod_deleted": { + "name": "is_mod_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_author_deleted": { + "name": "is_author_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "moderation_status": { + "name": "moderation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'approved'" + }, + "trust_status": { + "name": "trust_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'trusted'" + } + }, + "indexes": { + "topics_author_did_idx": { + "name": "topics_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_category_idx": { + "name": "topics_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_created_at_idx": { + "name": "topics_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_last_activity_at_idx": { + "name": "topics_last_activity_at_idx", + "columns": [ + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_community_did_idx": { + "name": "topics_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_moderation_status_idx": { + "name": "topics_moderation_status_idx", + "columns": [ + { + "expression": "moderation_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_trust_status_idx": { + "name": "topics_trust_status_idx", + "columns": [ + { + "expression": "trust_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_community_category_activity_idx": { + "name": "topics_community_category_activity_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_pinned_scope_idx": { + "name": "topics_pinned_scope_idx", + "columns": [ + { + "expression": "pinned_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "topics_author_did_rkey_idx": { + "name": "topics_author_did_rkey_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rkey", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.replies": { + "name": "replies", + "schema": "", + "columns": { + "uri": { + "name": "uri", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_format": { + "name": "content_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_uri": { + "name": "root_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "root_cid": { + "name": "root_cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_uri": { + "name": "parent_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_cid": { + "name": "parent_cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "labels": { + "name": "labels", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reaction_count": { + "name": "reaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vote_count": { + "name": "vote_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_author_deleted": { + "name": "is_author_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_mod_deleted": { + "name": "is_mod_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "moderation_status": { + "name": "moderation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'approved'" + }, + "trust_status": { + "name": "trust_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'trusted'" + } + }, + "indexes": { + "replies_author_did_idx": { + "name": "replies_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_root_uri_idx": { + "name": "replies_root_uri_idx", + "columns": [ + { + "expression": "root_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_parent_uri_idx": { + "name": "replies_parent_uri_idx", + "columns": [ + { + "expression": "parent_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_created_at_idx": { + "name": "replies_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_community_did_idx": { + "name": "replies_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_moderation_status_idx": { + "name": "replies_moderation_status_idx", + "columns": [ + { + "expression": "moderation_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_trust_status_idx": { + "name": "replies_trust_status_idx", + "columns": [ + { + "expression": "trust_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_root_uri_created_at_idx": { + "name": "replies_root_uri_created_at_idx", + "columns": [ + { + "expression": "root_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_root_uri_depth_idx": { + "name": "replies_root_uri_depth_idx", + "columns": [ + { + "expression": "root_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "replies_author_did_rkey_idx": { + "name": "replies_author_did_rkey_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rkey", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "uri": { + "name": "uri", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_uri": { + "name": "subject_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_cid": { + "name": "subject_cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reactions_author_did_idx": { + "name": "reactions_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_subject_uri_idx": { + "name": "reactions_subject_uri_idx", + "columns": [ + { + "expression": "subject_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_community_did_idx": { + "name": "reactions_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reactions_subject_uri_type_idx": { + "name": "reactions_subject_uri_type_idx", + "columns": [ + { + "expression": "subject_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reactions_author_subject_type_uniq": { + "name": "reactions_author_subject_type_uniq", + "nullsNotDistinct": false, + "columns": ["author_did", "subject_uri", "type"] + } + }, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.votes": { + "name": "votes", + "schema": "", + "columns": { + "uri": { + "name": "uri", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_uri": { + "name": "subject_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_cid": { + "name": "subject_cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "votes_author_did_idx": { + "name": "votes_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_subject_uri_idx": { + "name": "votes_subject_uri_idx", + "columns": [ + { + "expression": "subject_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_community_did_idx": { + "name": "votes_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "votes_author_subject_uniq": { + "name": "votes_author_subject_uniq", + "nullsNotDistinct": false, + "columns": ["author_did", "subject_uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracked_repos": { + "name": "tracked_repos", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tracked_at": { + "name": "tracked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_settings": { + "name": "community_settings", + "schema": "", + "columns": { + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "domains": { + "name": "domains", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "initialized": { + "name": "initialized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "admin_did": { + "name": "admin_did", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "community_name": { + "name": "community_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Barazo Community'" + }, + "maturity_rating": { + "name": "maturity_rating", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'safe'" + }, + "reaction_set": { + "name": "reaction_set", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"like\"]'::jsonb" + }, + "moderation_thresholds": { + "name": "moderation_thresholds", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"autoBlockReportCount\":5,\"warnThreshold\":3,\"firstPostQueueCount\":0,\"newAccountDays\":7,\"newAccountWriteRatePerMin\":3,\"establishedWriteRatePerMin\":10,\"linkHoldEnabled\":false,\"topicCreationDelayEnabled\":false,\"burstPostCount\":5,\"burstWindowMinutes\":10,\"trustedPostThreshold\":10}'::jsonb" + }, + "word_filter": { + "name": "word_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "jurisdiction_country": { + "name": "jurisdiction_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "age_threshold": { + "name": "age_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 16 + }, + "max_reply_depth": { + "name": "max_reply_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 9999 + }, + "require_login_for_mature": { + "name": "require_login_for_mature", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "community_description": { + "name": "community_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_endpoint": { + "name": "service_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_key": { + "name": "rotation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "community_logo_url": { + "name": "community_logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_logo_url": { + "name": "header_logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "show_community_name": { + "name": "show_community_name", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maturity_rating": { + "name": "maturity_rating", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'safe'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "categories_slug_community_did_idx": { + "name": "categories_slug_community_did_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_parent_id_idx": { + "name": "categories_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_community_did_idx": { + "name": "categories_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_maturity_rating_idx": { + "name": "categories_maturity_rating_idx", + "columns": [ + { + "expression": "maturity_rating", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_parent_id_fk": { + "name": "categories_parent_id_fk", + "tableFrom": "categories", + "tableTo": "categories", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.moderation_actions": { + "name": "moderation_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_uri": { + "name": "target_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_did": { + "name": "target_did", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moderator_did": { + "name": "moderator_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mod_actions_moderator_did_idx": { + "name": "mod_actions_moderator_did_idx", + "columns": [ + { + "expression": "moderator_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_actions_community_did_idx": { + "name": "mod_actions_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_actions_created_at_idx": { + "name": "mod_actions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_actions_target_uri_idx": { + "name": "mod_actions_target_uri_idx", + "columns": [ + { + "expression": "target_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_actions_target_did_idx": { + "name": "mod_actions_target_did_idx", + "columns": [ + { + "expression": "target_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "reporter_did": { + "name": "reporter_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_uri": { + "name": "target_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_did": { + "name": "target_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_type": { + "name": "reason_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "resolution_type": { + "name": "resolution_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "appeal_reason": { + "name": "appeal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appealed_at": { + "name": "appealed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "appeal_status": { + "name": "appeal_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_reporter_did_idx": { + "name": "reports_reporter_did_idx", + "columns": [ + { + "expression": "reporter_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_uri_idx": { + "name": "reports_target_uri_idx", + "columns": [ + { + "expression": "target_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_did_idx": { + "name": "reports_target_did_idx", + "columns": [ + { + "expression": "target_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_community_did_idx": { + "name": "reports_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_created_at_idx": { + "name": "reports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_unique_reporter_target_idx": { + "name": "reports_unique_reporter_target_idx", + "columns": [ + { + "expression": "reporter_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "recipient_did": { + "name": "recipient_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_uri": { + "name": "subject_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_did": { + "name": "actor_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_recipient_did_idx": { + "name": "notifications_recipient_did_idx", + "columns": [ + { + "expression": "recipient_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_recipient_read_idx": { + "name": "notifications_recipient_read_idx", + "columns": [ + { + "expression": "recipient_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_at_idx": { + "name": "notifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user_community_preferences": { + "name": "user_community_preferences", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maturity_override": { + "name": "maturity_override", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "muted_words": { + "name": "muted_words", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blocked_dids": { + "name": "blocked_dids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "muted_dids": { + "name": "muted_dids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notification_prefs": { + "name": "notification_prefs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_community_prefs_did_idx": { + "name": "user_community_prefs_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_community_prefs_community_idx": { + "name": "user_community_prefs_community_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_community_preferences_did_community_did_pk": { + "name": "user_community_preferences_did_community_did_pk", + "columns": ["did", "community_did"] + } + }, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "maturity_level": { + "name": "maturity_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sfw'" + }, + "declared_age": { + "name": "declared_age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "muted_words": { + "name": "muted_words", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "blocked_dids": { + "name": "blocked_dids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "muted_dids": { + "name": "muted_dids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cross_post_bluesky": { + "name": "cross_post_bluesky", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cross_post_frontpage": { + "name": "cross_post_frontpage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cross_post_scopes_granted": { + "name": "cross_post_scopes_granted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cross_posts": { + "name": "cross_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "topic_uri": { + "name": "topic_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cross_post_uri": { + "name": "cross_post_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cross_post_cid": { + "name": "cross_post_cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cross_posts_topic_uri_idx": { + "name": "cross_posts_topic_uri_idx", + "columns": [ + { + "expression": "topic_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cross_posts_author_did_idx": { + "name": "cross_posts_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_onboarding_fields": { + "name": "community_onboarding_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_mandatory": { + "name": "is_mandatory", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "onboarding_fields_community_idx": { + "name": "onboarding_fields_community_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.user_onboarding_responses": { + "name": "user_onboarding_responses", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "onboarding_responses_did_community_idx": { + "name": "onboarding_responses_did_community_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_onboarding_responses_did_community_did_field_id_pk": { + "name": "user_onboarding_responses_did_community_did_field_id_pk", + "columns": ["did", "community_did", "field_id"] + } + }, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.moderation_queue": { + "name": "moderation_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "content_uri": { + "name": "content_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_reason": { + "name": "queue_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "matched_words": { + "name": "matched_words", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mod_queue_author_did_idx": { + "name": "mod_queue_author_did_idx", + "columns": [ + { + "expression": "author_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_queue_community_did_idx": { + "name": "mod_queue_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_queue_status_idx": { + "name": "mod_queue_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_queue_created_at_idx": { + "name": "mod_queue_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_queue_content_uri_idx": { + "name": "mod_queue_content_uri_idx", + "columns": [ + { + "expression": "content_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.account_trust": { + "name": "account_trust", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_post_count": { + "name": "approved_post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_trusted": { + "name": "is_trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trusted_at": { + "name": "trusted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "account_trust_did_community_idx": { + "name": "account_trust_did_community_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_trust_did_idx": { + "name": "account_trust_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.community_filters": { + "name": "community_filters", + "schema": "", + "columns": { + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "admin_did": { + "name": "admin_did", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_count": { + "name": "report_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reviewed_at": { + "name": "last_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "filtered_by": { + "name": "filtered_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "community_filters_status_idx": { + "name": "community_filters_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "community_filters_admin_did_idx": { + "name": "community_filters_admin_did_idx", + "columns": [ + { + "expression": "admin_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "community_filters_updated_at_idx": { + "name": "community_filters_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.account_filters": { + "name": "account_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_count": { + "name": "report_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ban_count": { + "name": "ban_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reviewed_at": { + "name": "last_reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "filtered_by": { + "name": "filtered_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_filters_did_community_idx": { + "name": "account_filters_did_community_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_filters_did_idx": { + "name": "account_filters_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_filters_community_did_idx": { + "name": "account_filters_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_filters_status_idx": { + "name": "account_filters_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_filters_updated_at_idx": { + "name": "account_filters_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.ozone_labels": { + "name": "ozone_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "src": { + "name": "src", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "val": { + "name": "val", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "neg": { + "name": "neg", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cts": { + "name": "cts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "exp": { + "name": "exp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "indexed_at": { + "name": "indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ozone_labels_src_uri_val_idx": { + "name": "ozone_labels_src_uri_val_idx", + "columns": [ + { + "expression": "src", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "val", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ozone_labels_uri_idx": { + "name": "ozone_labels_uri_idx", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ozone_labels_val_idx": { + "name": "ozone_labels_val_idx", + "columns": [ + { + "expression": "val", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ozone_labels_indexed_at_idx": { + "name": "ozone_labels_indexed_at_idx", + "columns": [ + { + "expression": "indexed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_profiles": { + "name": "community_profiles", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "community_profiles_did_idx": { + "name": "community_profiles_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "community_profiles_community_idx": { + "name": "community_profiles_community_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "community_profiles_did_community_did_pk": { + "name": "community_profiles_did_community_did_pk", + "columns": ["did", "community_did"] + } + }, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.interaction_graph": { + "name": "interaction_graph", + "schema": "", + "columns": { + "source_did": { + "name": "source_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_did": { + "name": "target_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_id": { + "name": "community_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interaction_type": { + "name": "interaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "first_interaction_at": { + "name": "first_interaction_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_interaction_at": { + "name": "last_interaction_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_graph_source_target_community_idx": { + "name": "interaction_graph_source_target_community_idx", + "columns": [ + { + "expression": "source_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "interaction_graph_source_did_target_did_community_id_interaction_type_pk": { + "name": "interaction_graph_source_did_target_did_community_id_interaction_type_pk", + "columns": ["source_did", "target_did", "community_id", "interaction_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trust_seeds": { + "name": "trust_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_id": { + "name": "community_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trust_seeds_did_community_idx": { + "name": "trust_seeds_did_community_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trust_scores": { + "name": "trust_scores", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_id": { + "name": "community_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trust_scores_did_community_idx": { + "name": "trust_scores_did_community_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trust_scores_did_community_id_pk": { + "name": "trust_scores_did_community_id_pk", + "columns": ["did", "community_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sybil_clusters": { + "name": "sybil_clusters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cluster_hash": { + "name": "cluster_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_edge_count": { + "name": "internal_edge_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "external_edge_count": { + "name": "external_edge_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "member_count": { + "name": "member_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'flagged'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sybil_clusters_hash_idx": { + "name": "sybil_clusters_hash_idx", + "columns": [ + { + "expression": "cluster_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sybil_cluster_members": { + "name": "sybil_cluster_members", + "schema": "", + "columns": { + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_in_cluster": { + "name": "role_in_cluster", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sybil_cluster_members_cluster_id_sybil_clusters_id_fk": { + "name": "sybil_cluster_members_cluster_id_sybil_clusters_id_fk", + "tableFrom": "sybil_cluster_members", + "tableTo": "sybil_clusters", + "columnsFrom": ["cluster_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sybil_cluster_members_cluster_id_did_pk": { + "name": "sybil_cluster_members_cluster_id_did_pk", + "columns": ["cluster_id", "did"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.behavioral_flags": { + "name": "behavioral_flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "affected_dids": { + "name": "affected_dids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "behavioral_flags_flag_type_idx": { + "name": "behavioral_flags_flag_type_idx", + "columns": [ + { + "expression": "flag_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "behavioral_flags_status_idx": { + "name": "behavioral_flags_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "behavioral_flags_detected_at_idx": { + "name": "behavioral_flags_detected_at_idx", + "columns": [ + { + "expression": "detected_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pds_trust_factors": { + "name": "pds_trust_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pds_host": { + "name": "pds_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trust_factor": { + "name": "trust_factor", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pds_trust_factors_pds_host_idx": { + "name": "pds_trust_factors_pds_host_idx", + "columns": [ + { + "expression": "pds_host", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "community_did": { + "name": "community_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pages_slug_community_did_idx": { + "name": "pages_slug_community_did_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_community_did_idx": { + "name": "pages_community_did_idx", + "columns": [ + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_parent_id_idx": { + "name": "pages_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_status_community_did_idx": { + "name": "pages_status_community_did_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "community_did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pages_parent_id_fk": { + "name": "pages_parent_id_fk", + "tableFrom": "pages", + "tableTo": "pages", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "tenant_isolation": { + "name": "tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["barazo_app"], + "using": "community_did = current_setting('app.current_community_did', true)", + "withCheck": "community_did = current_setting('app.current_community_did', true)" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": { + "barazo_app": { + "name": "barazo_app", + "createDb": false, + "createRole": false, + "inherit": true + } + }, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 435491d..b1ba229 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1772706380033, "tag": "0008_add_author_rkey_indexes", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1772728814082, + "tag": "0009_brainy_sunspot", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/db/schema/topics.ts b/src/db/schema/topics.ts index 4b9b602..90f21a8 100644 --- a/src/db/schema/topics.ts +++ b/src/db/schema/topics.ts @@ -1,4 +1,13 @@ -import { pgTable, pgPolicy, text, integer, timestamp, jsonb, boolean, index } from 'drizzle-orm/pg-core' +import { + pgTable, + pgPolicy, + text, + integer, + timestamp, + jsonb, + boolean, + index, +} from 'drizzle-orm/pg-core' import { sql } from 'drizzle-orm' import { appRole } from './roles.js' @@ -24,6 +33,8 @@ export const topics = pgTable( indexedAt: timestamp('indexed_at', { withTimezone: true }).notNull().defaultNow(), isLocked: boolean('is_locked').notNull().default(false), isPinned: boolean('is_pinned').notNull().default(false), + pinnedAt: timestamp('pinned_at', { withTimezone: true }), + pinnedScope: text('pinned_scope', { enum: ['category', 'forum'] }), isModDeleted: boolean('is_mod_deleted').notNull().default(false), isAuthorDeleted: boolean('is_author_deleted').notNull().default(false), moderationStatus: text('moderation_status', { @@ -55,6 +66,7 @@ export const topics = pgTable( table.category, table.lastActivityAt ), + index('topics_pinned_scope_idx').on(table.pinnedScope), index('topics_author_did_rkey_idx').on(table.authorDid, table.rkey), pgPolicy('tenant_isolation', { as: 'permissive', diff --git a/src/routes/moderation.ts b/src/routes/moderation.ts index 094e49b..5aec85a 100644 --- a/src/routes/moderation.ts +++ b/src/routes/moderation.ts @@ -260,6 +260,7 @@ export function moderationRoutes(): FastifyPluginCallback { type: 'object', properties: { reason: { type: 'string', maxLength: 500 }, + scope: { type: 'string', enum: ['category', 'forum'] }, }, }, response: { @@ -268,6 +269,8 @@ export function moderationRoutes(): FastifyPluginCallback { properties: { uri: { type: 'string' }, isPinned: { type: 'boolean' }, + pinnedScope: { type: 'string', nullable: true }, + pinnedAt: { type: 'string', nullable: true }, }, }, 401: errorResponseSchema, @@ -286,6 +289,7 @@ export function moderationRoutes(): FastifyPluginCallback { const { id } = request.params as { id: string } const decodedUri = decodeURIComponent(id) const parsed = pinTopicSchema.safeParse(request.body) + const scope = parsed.success ? parsed.data.scope : 'category' const topicRows = await db .select() @@ -300,8 +304,23 @@ export function moderationRoutes(): FastifyPluginCallback { const newPinned = !topic.isPinned const action = newPinned ? 'pin' : 'unpin' + // Forum-wide pins require admin role + if (newPinned && scope === 'forum') { + const userRows = await db.select().from(users).where(eq(users.did, user.did)) + const userRecord = userRows[0] + if (!userRecord || userRecord.role !== 'admin') { + throw forbidden('Forum-wide pins require admin privileges') + } + } + + const pinnedAt = newPinned ? new Date() : null + const pinnedScope = newPinned ? scope : null + await db.transaction(async (tx) => { - await tx.update(topics).set({ isPinned: newPinned }).where(eq(topics.uri, decodedUri)) + await tx + .update(topics) + .set({ isPinned: newPinned, pinnedAt, pinnedScope }) + .where(eq(topics.uri, decodedUri)) await tx.insert(moderationActions).values({ action, @@ -312,7 +331,10 @@ export function moderationRoutes(): FastifyPluginCallback { }) }) - app.log.info({ action, topicUri: decodedUri, moderatorDid: user.did }, `Topic ${action}ned`) + app.log.info( + { action, topicUri: decodedUri, moderatorDid: user.did, pinnedScope }, + `Topic ${action}ned` + ) // Fire-and-forget: notify topic author of pin/unpin notificationService @@ -329,6 +351,8 @@ export function moderationRoutes(): FastifyPluginCallback { return reply.status(200).send({ uri: decodedUri, isPinned: newPinned, + pinnedScope, + pinnedAt: pinnedAt?.toISOString() ?? null, }) } ) diff --git a/src/routes/topics.ts b/src/routes/topics.ts index 12bf757..04cb747 100644 --- a/src/routes/topics.ts +++ b/src/routes/topics.ts @@ -1,4 +1,4 @@ -import { eq, and, desc, sql, inArray, notInArray, isNotNull, ne, or } from 'drizzle-orm' +import { eq, and, asc, desc, sql, inArray, notInArray, isNotNull, ne, or } from 'drizzle-orm' import { requireCommunityDid } from '../middleware/community-resolver.js' import type { FastifyPluginCallback } from 'fastify' import { createPdsClient } from '../lib/pds-client.js' @@ -84,6 +84,10 @@ const topicJsonSchema = { isMuted: { type: 'boolean' as const }, isMutedWord: { type: 'boolean' as const }, ozoneLabel: { type: ['string', 'null'] as const }, + isPinned: { type: 'boolean' as const }, + isLocked: { type: 'boolean' as const }, + pinnedScope: { type: ['string', 'null'] as const }, + pinnedAt: { type: ['string', 'null'] as const, format: 'date-time' as const }, categoryMaturityRating: { type: 'string' as const, enum: ['safe', 'mature', 'adult'] }, lastActivityAt: { type: 'string' as const, format: 'date-time' as const }, createdAt: { type: 'string' as const, format: 'date-time' as const }, @@ -123,6 +127,10 @@ function serializeTopic(row: typeof topics.$inferSelect, categoryMaturityRating: reactionCount: row.reactionCount, isAuthorDeleted: row.isAuthorDeleted, isModDeleted: row.isModDeleted, + isPinned: row.isPinned, + isLocked: row.isLocked, + pinnedScope: row.pinnedScope ?? null, + pinnedAt: row.pinnedAt?.toISOString() ?? null, categoryMaturityRating, lastActivityAt: row.lastActivityAt.toISOString(), createdAt: row.createdAt.toISOString(), @@ -707,11 +715,21 @@ export function topicRoutes(): FastifyPluginCallback { // score = (reply_count + reaction_count * 0.3) / (age_in_hours + 2) ^ 1.2 const popularityScore = sql`(${topics.replyCount} + ${topics.reactionCount} * 0.3) / POWER(EXTRACT(EPOCH FROM (NOW() - ${topics.createdAt})) / 3600.0 + 2, 1.2)` + // Pinned-first ordering: when browsing a category, both category-pinned + // and forum-pinned topics float to the top. On the homepage (no category + // filter), only forum-pinned topics get promoted. + const pinnedFirst = category + ? sql`CASE WHEN ${topics.pinnedScope} IS NOT NULL THEN 0 ELSE 1 END` + : sql`CASE WHEN ${topics.pinnedScope} = 'forum' THEN 0 ELSE 1 END` + const rows = await db .select() .from(topics) .where(whereClause) - .orderBy(sort === 'popular' ? desc(popularityScore) : desc(topics.lastActivityAt)) + .orderBy( + asc(pinnedFirst), + sort === 'popular' ? desc(popularityScore) : desc(topics.lastActivityAt) + ) .limit(fetchLimit) const hasMore = rows.length > limit diff --git a/src/validation/moderation.ts b/src/validation/moderation.ts index 926812c..9399739 100644 --- a/src/validation/moderation.ts +++ b/src/validation/moderation.ts @@ -10,6 +10,7 @@ export const lockTopicSchema = z.object({ export const pinTopicSchema = z.object({ reason: z.string().max(500).optional(), + scope: z.enum(['category', 'forum']).default('category'), }) export const modDeleteSchema = z.object({ diff --git a/tests/integration/tenant-isolation.test.ts b/tests/integration/tenant-isolation.test.ts index a42f9c9..16cafb3 100644 --- a/tests/integration/tenant-isolation.test.ts +++ b/tests/integration/tenant-isolation.test.ts @@ -90,6 +90,8 @@ async function pushSchema(client: ReturnType): Promise { indexed_at TIMESTAMPTZ NOT NULL DEFAULT now(), is_locked BOOLEAN NOT NULL DEFAULT false, is_pinned BOOLEAN NOT NULL DEFAULT false, + pinned_at TIMESTAMPTZ, + pinned_scope TEXT, is_mod_deleted BOOLEAN NOT NULL DEFAULT false, is_author_deleted BOOLEAN NOT NULL DEFAULT false, moderation_status TEXT NOT NULL DEFAULT 'approved', diff --git a/tests/unit/db/schema/topics.test.ts b/tests/unit/db/schema/topics.test.ts index 0063661..2843809 100644 --- a/tests/unit/db/schema/topics.test.ts +++ b/tests/unit/db/schema/topics.test.ts @@ -82,4 +82,16 @@ describe('topics schema', () => { expect(columns.isPinned.hasDefault).toBe(true) expect(columns.isModDeleted.hasDefault).toBe(true) }) + + it('has nullable pinnedAt timestamp column', () => { + const columnNames = Object.keys(columns) + expect(columnNames).toContain('pinnedAt') + expect(columns.pinnedAt.notNull).toBe(false) + }) + + it('has nullable pinnedScope text column', () => { + const columnNames = Object.keys(columns) + expect(columnNames).toContain('pinnedScope') + expect(columns.pinnedScope.notNull).toBe(false) + }) }) diff --git a/tests/unit/routes/moderation.test.ts b/tests/unit/routes/moderation.test.ts index 14dadac..15de562 100644 --- a/tests/unit/routes/moderation.test.ts +++ b/tests/unit/routes/moderation.test.ts @@ -173,6 +173,8 @@ function sampleTopicRow(overrides?: Record) { indexedAt: new Date(TEST_NOW), isLocked: false, isPinned: false, + pinnedAt: null, + pinnedScope: null, isModDeleted: false, embedding: null, ...overrides, @@ -506,6 +508,77 @@ describe('moderation routes', () => { expect(response.statusCode).toBe(404) }) + + it('should pin with category scope by default', async () => { + selectChain.where.mockResolvedValueOnce([sampleTopicRow({ isPinned: false })]) + + const encodedUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'POST', + url: `/api/moderation/pin/${encodedUri}`, + headers: { authorization: 'Bearer test-token' }, + payload: {}, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ uri: string; isPinned: boolean; pinnedScope: string | null }>() + expect(body.isPinned).toBe(true) + expect(body.pinnedScope).toBe('category') + }) + + it('should pin with forum scope', async () => { + // User lookup for admin check -- return admin role + selectChain.where.mockResolvedValueOnce([sampleTopicRow({ isPinned: false })]) + selectChain.where.mockResolvedValueOnce([sampleUserRow({ did: TEST_DID, role: 'admin' })]) + + const encodedUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'POST', + url: `/api/moderation/pin/${encodedUri}`, + headers: { authorization: 'Bearer test-token' }, + payload: { scope: 'forum' }, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ uri: string; isPinned: boolean; pinnedScope: string | null }>() + expect(body.isPinned).toBe(true) + expect(body.pinnedScope).toBe('forum') + }) + + it('should clear pinnedScope and pinnedAt when unpinning', async () => { + selectChain.where.mockResolvedValueOnce([ + sampleTopicRow({ isPinned: true, pinnedAt: new Date(TEST_NOW), pinnedScope: 'category' }), + ]) + + const encodedUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'POST', + url: `/api/moderation/pin/${encodedUri}`, + headers: { authorization: 'Bearer test-token' }, + payload: {}, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ uri: string; isPinned: boolean; pinnedScope: string | null }>() + expect(body.isPinned).toBe(false) + expect(body.pinnedScope).toBeNull() + }) + + it('should reject forum-wide pin from non-admin moderator', async () => { + selectChain.where.mockResolvedValueOnce([sampleTopicRow({ isPinned: false })]) + // User lookup returns moderator role (not admin) + selectChain.where.mockResolvedValueOnce([sampleUserRow({ did: TEST_DID, role: 'moderator' })]) + + const encodedUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'POST', + url: `/api/moderation/pin/${encodedUri}`, + headers: { authorization: 'Bearer test-token' }, + payload: { scope: 'forum' }, + }) + + expect(response.statusCode).toBe(403) + }) }) // ========================================================================= diff --git a/tests/unit/routes/topics.test.ts b/tests/unit/routes/topics.test.ts index eef2321..07565db 100644 --- a/tests/unit/routes/topics.test.ts +++ b/tests/unit/routes/topics.test.ts @@ -225,6 +225,10 @@ function sampleTopicRow(overrides?: Record) { labels: null, replyCount: 0, reactionCount: 0, + isPinned: false, + isLocked: false, + pinnedScope: null, + pinnedAt: null, lastActivityAt: new Date(TEST_NOW), createdAt: new Date(TEST_NOW), indexedAt: new Date(TEST_NOW), @@ -868,6 +872,58 @@ describe('topic routes', () => { await noAuthApp.close() }) + it('includes pinned topics in list response with pinned fields', async () => { + setupMaturityMocks(true) + const pinnedRow = sampleTopicRow({ + uri: `at://${TEST_DID}/forum.barazo.topic.post/pinned1`, + rkey: 'pinned1', + isPinned: true, + pinnedScope: 'forum', + pinnedAt: new Date('2026-02-14T00:00:00.000Z'), + }) + const normalRow = sampleTopicRow({ + uri: `at://${TEST_DID}/forum.barazo.topic.post/normal1`, + rkey: 'normal1', + lastActivityAt: new Date('2026-02-15T00:00:00.000Z'), + }) + selectChain.limit.mockResolvedValueOnce([pinnedRow, normalRow]) + + const response = await app.inject({ + method: 'GET', + url: '/api/topics', + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + topics: Array<{ + uri: string + isPinned: boolean + pinnedScope: string | null + pinnedAt: string | null + }> + }>() + expect(body.topics).toHaveLength(2) + expect(body.topics[0]?.isPinned).toBe(true) + expect(body.topics[0]?.pinnedScope).toBe('forum') + expect(body.topics[0]?.pinnedAt).toBe('2026-02-14T00:00:00.000Z') + expect(body.topics[1]?.isPinned).toBe(false) + expect(body.topics[1]?.pinnedScope).toBeNull() + }) + + it('calls orderBy for pinned-first sorting', async () => { + setupMaturityMocks(true) + selectChain.limit.mockResolvedValueOnce([]) + + const response = await app.inject({ + method: 'GET', + url: '/api/topics', + }) + + expect(response.statusCode).toBe(200) + // The query chain goes .where().orderBy().limit(), so orderBy must have been called + expect(selectChain.orderBy).toHaveBeenCalled() + }) + it('includes author profile in topic response', async () => { resetAllDbMocks() setupMaturityMocks(true) @@ -2724,6 +2780,68 @@ describe('topic routes', () => { expect(body.topics[0]?.tags).toBeNull() }) + it('includes isPinned, isLocked, pinnedScope, and pinnedAt in response', async () => { + setupMaturityMocks(true) + + const pinnedDate = new Date('2026-03-01T12:00:00.000Z') + const rows = [ + sampleTopicRow({ + isPinned: true, + isLocked: true, + pinnedScope: 'category', + pinnedAt: pinnedDate, + }), + ] + selectChain.limit.mockResolvedValueOnce(rows) + + const response = await app.inject({ + method: 'GET', + url: '/api/topics', + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + topics: Array<{ + isPinned: boolean + isLocked: boolean + pinnedScope: string | null + pinnedAt: string | null + }> + }>() + expect(body.topics).toHaveLength(1) + expect(body.topics[0]?.isPinned).toBe(true) + expect(body.topics[0]?.isLocked).toBe(true) + expect(body.topics[0]?.pinnedScope).toBe('category') + expect(body.topics[0]?.pinnedAt).toBe('2026-03-01T12:00:00.000Z') + }) + + it('returns null for pinnedScope and pinnedAt when topic is not pinned', async () => { + setupMaturityMocks(true) + + const rows = [sampleTopicRow()] + selectChain.limit.mockResolvedValueOnce(rows) + + const response = await app.inject({ + method: 'GET', + url: '/api/topics', + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + topics: Array<{ + isPinned: boolean + isLocked: boolean + pinnedScope: string | null + pinnedAt: string | null + }> + }>() + expect(body.topics).toHaveLength(1) + expect(body.topics[0]?.isPinned).toBe(false) + expect(body.topics[0]?.isLocked).toBe(false) + expect(body.topics[0]?.pinnedScope).toBeNull() + expect(body.topics[0]?.pinnedAt).toBeNull() + }) + it('provides fallback author profile when author not found in DB', async () => { resetAllDbMocks() setupMaturityMocks(true) -- 2.51.2