diff --git a/drizzle/0002_perfect_the_captain.sql b/drizzle/0002_perfect_the_captain.sql new file mode 100644 index 0000000..e74abb9 --- /dev/null +++ b/drizzle/0002_perfect_the_captain.sql @@ -0,0 +1 @@ +ALTER TABLE "community_onboarding_fields" ADD COLUMN "source" text DEFAULT 'admin' NOT NULL; \ No newline at end of file diff --git a/drizzle/0003_backfill-platform-age-field.sql b/drizzle/0003_backfill-platform-age-field.sql new file mode 100644 index 0000000..0fc6145 --- /dev/null +++ b/drizzle/0003_backfill-platform-age-field.sql @@ -0,0 +1,48 @@ +-- Seed platform age_confirmation field for all initialized communities +-- that don't already have an admin-created age_confirmation field. +-- NOTE: assumes single-community mode (P1/P2). Multi-community (P3) +-- will need per-community unique IDs. +INSERT INTO community_onboarding_fields ( + id, community_did, field_type, label, description, + is_mandatory, sort_order, source, config, created_at, updated_at +) +SELECT + 'platform:age_confirmation', + cs.community_did, + 'age_confirmation', + 'Age Declaration', + 'Please select your age bracket. This determines which content is available to you.', + true, + -1, + 'platform', + NULL, + NOW(), + NOW() +FROM community_settings cs +WHERE cs.initialized = true + AND NOT EXISTS ( + SELECT 1 FROM community_onboarding_fields cof + WHERE cof.community_did = cs.community_did + AND cof.field_type = 'age_confirmation' + ) +ON CONFLICT DO NOTHING; + +-- Backfill user_onboarding_responses for users who already declared age +-- via the virtual system field (data lives in user_preferences.declared_age). +INSERT INTO user_onboarding_responses (did, community_did, field_id, response, completed_at) +SELECT + up.did, + cs.community_did, + 'platform:age_confirmation', + to_jsonb(up.declared_age), + COALESCE(up.updated_at, NOW()) +FROM user_preferences up +CROSS JOIN community_settings cs +WHERE up.declared_age IS NOT NULL + AND cs.initialized = true + AND EXISTS ( + SELECT 1 FROM community_onboarding_fields cof + WHERE cof.id = 'platform:age_confirmation' + AND cof.community_did = cs.community_did + ) +ON CONFLICT DO NOTHING; diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..56b1c87 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,3842 @@ +{ + "id": "5f8531c6-1108-4540-8776-0cb9de6ab736", + "prevId": "162ade1e-0657-4007-8e90-3bef3a52306c", + "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 + }, + "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": {} + } + }, + "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 + }, + "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": {} + } + }, + "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\":3,\"newAccountDays\":7,\"newAccountWriteRatePerMin\":3,\"establishedWriteRatePerMin\":10,\"linkHoldEnabled\":true,\"topicCreationDelayEnabled\":true,\"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 + }, + "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 + }, + "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 + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": { + "barazo_app": { + "name": "barazo_app", + "createDb": false, + "createRole": false, + "inherit": true + } + }, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 31dcfaa..ea1dafd 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,20 @@ "when": 1772555130464, "tag": "0001_add_favicon_url", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1772610945005, + "tag": "0002_perfect_the_captain", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1772556676000, + "tag": "0003_backfill-platform-age-field", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/config/env.ts b/src/config/env.ts index ff85811..8339ee6 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -37,6 +37,7 @@ const baseEnvSchema = z.object({ // Community COMMUNITY_MODE: z.enum(['single', 'multi']).default('single'), + HOSTING_MODE: z.enum(['saas', 'selfhosted']).default('selfhosted'), COMMUNITY_DID: z.string().optional(), COMMUNITY_NAME: z.string().default('Barazo Community'), diff --git a/src/db/schema/onboarding-fields.ts b/src/db/schema/onboarding-fields.ts index 1614555..c76195f 100644 --- a/src/db/schema/onboarding-fields.ts +++ b/src/db/schema/onboarding-fields.ts @@ -33,6 +33,9 @@ export const communityOnboardingFields = pgTable( description: text('description'), isMandatory: boolean('is_mandatory').notNull().default(true), sortOrder: integer('sort_order').notNull().default(0), + source: text('source', { + enum: ['platform', 'admin'], + }).notNull().default('admin'), config: jsonb('config').$type>(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), diff --git a/src/lib/onboarding-gate.ts b/src/lib/onboarding-gate.ts index 71f3241..472992b 100644 --- a/src/lib/onboarding-gate.ts +++ b/src/lib/onboarding-gate.ts @@ -3,11 +3,8 @@ import { communityOnboardingFields, userOnboardingResponses, } from '../db/schema/onboarding-fields.js' -import { userPreferences } from '../db/schema/user-preferences.js' import type { Database } from '../db/index.js' -const SYSTEM_AGE_FIELD_ID = 'system-age-confirmation' - export interface OnboardingCheckResult { complete: boolean missingFields: { id: string; label: string; fieldType: string }[] @@ -15,19 +12,17 @@ export interface OnboardingCheckResult { /** * Check whether a user has completed all mandatory onboarding fields - * for a community. Returns complete=true if no fields are configured - * or all mandatory ones have responses. + * for a community. Returns complete=true if no mandatory fields are + * configured or all have responses. * - * Also checks for the system-level age declaration: if no admin-configured - * age_confirmation field exists and the user has no declaredAge, the system - * age field is treated as a missing mandatory field. + * All fields (platform and admin) live in the database -- no virtual + * field injection needed. */ export async function checkOnboardingComplete( db: Database, did: string, communityDid: string ): Promise { - // Get mandatory fields for this community const fields = await db .select() .from(communityOnboardingFields) @@ -38,49 +33,17 @@ export async function checkOnboardingComplete( ) ) - // Get user's responses for this community const responses = await db .select() .from(userOnboardingResponses) .where( - and( - eq(userOnboardingResponses.did, did), - eq(userOnboardingResponses.communityDid, communityDid) - ) + and(eq(userOnboardingResponses.did, did), eq(userOnboardingResponses.communityDid, communityDid)) ) const answeredFieldIds = new Set(responses.map((r) => r.fieldId)) - const missingFields = fields .filter((f) => !answeredFieldIds.has(f.id)) .map((f) => ({ id: f.id, label: f.label, fieldType: f.fieldType })) - // Check for system-level age field: inject if no admin age field and user has no declaredAge - const allCommunityFields = await db - .select({ fieldType: communityOnboardingFields.fieldType }) - .from(communityOnboardingFields) - .where(eq(communityOnboardingFields.communityDid, communityDid)) - - const hasAdminAgeField = allCommunityFields.some((f) => f.fieldType === 'age_confirmation') - - if (!hasAdminAgeField) { - const prefRows = await db - .select({ declaredAge: userPreferences.declaredAge }) - .from(userPreferences) - .where(eq(userPreferences.did, did)) - - const declaredAge = prefRows[0]?.declaredAge ?? null - if (declaredAge === null) { - missingFields.unshift({ - id: SYSTEM_AGE_FIELD_ID, - label: 'Age Declaration', - fieldType: 'age_confirmation', - }) - } - } - - return { - complete: missingFields.length === 0, - missingFields, - } + return { complete: missingFields.length === 0, missingFields } } diff --git a/src/routes/onboarding.ts b/src/routes/onboarding.ts index 63cf3c4..1c99886 100644 --- a/src/routes/onboarding.ts +++ b/src/routes/onboarding.ts @@ -16,7 +16,6 @@ import { } from '../db/schema/onboarding-fields.js' import { userPreferences } from '../db/schema/user-preferences.js' import { users } from '../db/schema/users.js' -import { ageDeclarationSchema } from '../validation/profiles.js' // --------------------------------------------------------------------------- // OpenAPI JSON Schema definitions @@ -32,6 +31,7 @@ const onboardingFieldJsonSchema = { description: { type: ['string', 'null'] as const }, isMandatory: { type: 'boolean' as const }, sortOrder: { type: 'integer' as const }, + source: { type: 'string' as const, enum: ['platform', 'admin'] }, config: { type: ['object', 'null'] as const }, createdAt: { type: 'string' as const, format: 'date-time' as const }, updatedAt: { type: 'string' as const, format: 'date-time' as const }, @@ -56,12 +56,6 @@ const onboardingStatusJsonSchema = { }, } -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const SYSTEM_AGE_FIELD_ID = 'system-age-confirmation' - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -75,6 +69,7 @@ function serializeField(row: typeof communityOnboardingFields.$inferSelect) { description: row.description ?? null, isMandatory: row.isMandatory, sortOrder: row.sortOrder, + source: row.source, config: row.config ?? null, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), @@ -87,7 +82,7 @@ function serializeField(row: typeof communityOnboardingFields.$inferSelect) { export function onboardingRoutes(): FastifyPluginCallback { return (app, _opts, done) => { - const { db, authMiddleware } = app + const { db, env, authMiddleware } = app const requireAdmin = app.requireAdmin // ===================================================================== @@ -108,8 +103,11 @@ export function onboardingRoutes(): FastifyPluginCallback { security: [{ bearerAuth: [] }], response: { 200: { - type: 'array' as const, - items: onboardingFieldJsonSchema, + type: 'object' as const, + properties: { + fields: { type: 'array' as const, items: onboardingFieldJsonSchema }, + hostingMode: { type: 'string' as const, enum: ['saas', 'selfhosted'] }, + }, }, 401: errorResponseSchema, 403: errorResponseSchema, @@ -125,7 +123,10 @@ export function onboardingRoutes(): FastifyPluginCallback { .where(eq(communityOnboardingFields.communityDid, communityDid)) .orderBy(asc(communityOnboardingFields.sortOrder)) - return reply.status(200).send(fields.map(serializeField)) + return reply.status(200).send({ + fields: fields.map(serializeField), + hostingMode: env.HOSTING_MODE, + }) } ) @@ -251,6 +252,22 @@ export function onboardingRoutes(): FastifyPluginCallback { const communityDid = requireCommunityDid(request) + // SaaS guard: platform fields cannot be modified in SaaS mode + if (env.HOSTING_MODE === 'saas') { + const existing = await db + .select({ source: communityOnboardingFields.source }) + .from(communityOnboardingFields) + .where( + and( + eq(communityOnboardingFields.id, request.params.id), + eq(communityOnboardingFields.communityDid, communityDid) + ) + ) + if (existing[0]?.source === 'platform') { + throw forbidden('Platform fields cannot be modified in SaaS mode') + } + } + const dbUpdates: Record = { updatedAt: new Date() } if (updates.label !== undefined) dbUpdates.label = updates.label if (updates.description !== undefined) dbUpdates.description = updates.description @@ -309,6 +326,22 @@ export function onboardingRoutes(): FastifyPluginCallback { async (request, reply) => { const communityDid = requireCommunityDid(request) + // SaaS guard: platform fields cannot be deleted in SaaS mode + if (env.HOSTING_MODE === 'saas') { + const existing = await db + .select({ source: communityOnboardingFields.source }) + .from(communityOnboardingFields) + .where( + and( + eq(communityOnboardingFields.id, request.params.id), + eq(communityOnboardingFields.communityDid, communityDid) + ) + ) + if (existing[0]?.source === 'platform') { + throw forbidden('Platform fields cannot be deleted in SaaS mode') + } + } + const deleted = await db .delete(communityOnboardingFields) .where( @@ -433,7 +466,7 @@ export function onboardingRoutes(): FastifyPluginCallback { const communityDid = requireCommunityDid(request) - // Get all fields for this community + // Get all fields for this community (platform + admin, all from DB) const fields = await db .select() .from(communityOnboardingFields) @@ -453,67 +486,18 @@ export function onboardingRoutes(): FastifyPluginCallback { const responseMap = new Map(responses.map((r) => [r.fieldId, r.response])) - // Check if we need to inject a system-level age_confirmation field - const hasAdminAgeField = fields.some((f) => f.fieldType === 'age_confirmation') - - // Look up user's declared age - const prefRows = await db - .select({ declaredAge: userPreferences.declaredAge }) - .from(userPreferences) - .where(eq(userPreferences.did, user.did)) - - const declaredAge = prefRows[0]?.declaredAge ?? null - const needsSystemAgeField = !hasAdminAgeField && declaredAge === null - - type FieldWithStatus = { - id: string - communityDid: string - fieldType: string - label: string - description: string | null - isMandatory: boolean - sortOrder: number - config: Record | null - createdAt: string - updatedAt: string - completed: boolean - response: unknown - } - - const fieldsWithStatus: FieldWithStatus[] = fields.map((field) => ({ + const fieldsWithStatus = fields.map((field) => ({ ...serializeField(field), completed: responseMap.has(field.id), response: responseMap.get(field.id) ?? null, })) - // Inject system age field at the beginning if needed - if (needsSystemAgeField) { - const now = new Date().toISOString() - fieldsWithStatus.unshift({ - id: SYSTEM_AGE_FIELD_ID, - communityDid, - fieldType: 'age_confirmation', - label: 'Age Declaration', - description: - 'Please select your age bracket. This determines which content is available to you.', - isMandatory: true, - sortOrder: -1, - config: null, - createdAt: now, - updatedAt: now, - completed: false, - response: null, - }) - } - - // Check completeness: all mandatory fields (including system age field) must be answered const mandatoryFieldsComplete = fields .filter((f) => f.isMandatory) .every((f) => responseMap.has(f.id)) - const complete = mandatoryFieldsComplete && !needsSystemAgeField return reply.status(200).send({ - complete, + complete: mandatoryFieldsComplete, fields: fieldsWithStatus, }) } @@ -576,13 +560,9 @@ export function onboardingRoutes(): FastifyPluginCallback { const fieldMap = new Map(fields.map((f) => [f.id, f])) - // Separate system-level age submissions from admin-configured ones - const systemAgeSubmission = parsed.data.find((s) => s.fieldId === SYSTEM_AGE_FIELD_ID) - const adminSubmissions = parsed.data.filter((s) => s.fieldId !== SYSTEM_AGE_FIELD_ID) - - // Validate admin-configured field responses + // Validate all field responses uniformly (platform + admin fields alike) const errors: string[] = [] - for (const submission of adminSubmissions) { + for (const submission of parsed.data) { const field = fieldMap.get(submission.fieldId) if (!field) { errors.push(`Unknown field: ${submission.fieldId}`) @@ -595,35 +575,15 @@ export function onboardingRoutes(): FastifyPluginCallback { } } - // Validate system age submission - if (systemAgeSubmission) { - const ageParsed = ageDeclarationSchema.safeParse({ - declaredAge: systemAgeSubmission.response, - }) - if (!ageParsed.success) { - errors.push('Age Declaration: must be one of 0, 13, 14, 15, 16, 18') - } - } - - // Also validate admin-configured age_confirmation fields the same way - for (const submission of adminSubmissions) { - const field = fieldMap.get(submission.fieldId) - if (field?.fieldType === 'age_confirmation') { - const ageParsed = ageDeclarationSchema.safeParse({ - declaredAge: submission.response, - }) - if (!ageParsed.success) { - errors.push(`${field.label}: must be one of 0, 13, 14, 15, 16, 18`) - } - } - } - if (errors.length > 0) { throw badRequest(errors.join('; ')) } - // Upsert admin-field responses (idempotent) - for (const submission of adminSubmissions) { + // Upsert field responses (idempotent) + for (const submission of parsed.data) { + const field = fieldMap.get(submission.fieldId) + if (!field) continue + await db .insert(userOnboardingResponses) .values({ @@ -644,9 +604,8 @@ export function onboardingRoutes(): FastifyPluginCallback { }, }) - // Sync age_confirmation to user preferences - const field = fieldMap.get(submission.fieldId) - if (field?.fieldType === 'age_confirmation' && typeof submission.response === 'number') { + // Sync age_confirmation to user preferences + users table + if (field.fieldType === 'age_confirmation' && typeof submission.response === 'number') { const now = new Date() await db .insert(userPreferences) @@ -662,22 +621,6 @@ export function onboardingRoutes(): FastifyPluginCallback { } } - // Handle system-level age submission (syncs to user_preferences + users) - if (systemAgeSubmission && typeof systemAgeSubmission.response === 'number') { - const declaredAge = systemAgeSubmission.response - const now = new Date() - - await db - .insert(userPreferences) - .values({ did: user.did, declaredAge, updatedAt: now }) - .onConflictDoUpdate({ - target: userPreferences.did, - set: { declaredAge, updatedAt: now }, - }) - - await db.update(users).set({ declaredAge }).where(eq(users.did, user.did)) - } - // Check completeness (all mandatory fields answered?) const existingResponses = await db .select() @@ -690,16 +633,10 @@ export function onboardingRoutes(): FastifyPluginCallback { ) const answeredFieldIds = new Set(existingResponses.map((r) => r.fieldId)) - const adminFieldsComplete = fields + const complete = fields .filter((f) => f.isMandatory) .every((f) => answeredFieldIds.has(f.id)) - // System age field counts as complete if user now has a declaredAge - const systemAgeComplete = systemAgeSubmission - ? typeof systemAgeSubmission.response === 'number' - : true - const complete = adminFieldsComplete && systemAgeComplete - app.log.info( { event: 'onboarding_submitted', diff --git a/src/setup/service.ts b/src/setup/service.ts index 1149c21..e723a47 100644 --- a/src/setup/service.ts +++ b/src/setup/service.ts @@ -1,5 +1,6 @@ import { eq, sql } from 'drizzle-orm' import { communitySettings } from '../db/schema/community-settings.js' +import { communityOnboardingFields } from '../db/schema/onboarding-fields.js' import { users } from '../db/schema/users.js' import type { Database } from '../db/index.js' import { encrypt } from '../lib/encryption.js' @@ -187,6 +188,24 @@ export function createSetupService( await db.update(users).set({ role: 'admin' }).where(eq(users.did, did)) logger.info({ did }, 'User promoted to admin role') + // Seed platform onboarding fields + await db + .insert(communityOnboardingFields) + .values({ + id: 'platform:age_confirmation', + communityDid, + fieldType: 'age_confirmation', + label: 'Age Declaration', + description: + 'Please select your age bracket. This determines which content is available to you.', + isMandatory: true, + sortOrder: -1, + source: 'platform', + config: null, + }) + .onConflictDoNothing() + logger.info({ communityDid }, 'Platform onboarding fields seeded') + const finalName = row.communityName logger.info({ did, communityName: finalName }, 'Community initialized') diff --git a/tests/unit/config/env.test.ts b/tests/unit/config/env.test.ts index ff90f76..3f58040 100644 --- a/tests/unit/config/env.test.ts +++ b/tests/unit/config/env.test.ts @@ -311,6 +311,42 @@ describe('AI_ENCRYPTION_KEY validation', () => { }) }) +describe('HOSTING_MODE validation', () => { + const baseEnv = { + DATABASE_URL: 'postgresql://barazo:barazo_dev@localhost:5432/barazo', + VALKEY_URL: 'redis://localhost:6379', + TAP_URL: 'http://localhost:2480', + TAP_ADMIN_PASSWORD: 'tap_dev_secret', + OAUTH_CLIENT_ID: + 'http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fapi%2Fauth%2Fcallback', + OAUTH_REDIRECT_URI: 'http://127.0.0.1:3000/api/auth/callback', + SESSION_SECRET: 'a-very-long-session-secret-that-is-at-least-32-characters', + AI_ENCRYPTION_KEY: 'a-very-long-encryption-key-that-is-at-least-32-characters', + COMMUNITY_DID: 'did:plc:testcommunity123', + } + + it('defaults to selfhosted when HOSTING_MODE is omitted', () => { + const result = envSchema.safeParse(baseEnv) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.HOSTING_MODE).toBe('selfhosted') + } + }) + + it('accepts saas as HOSTING_MODE', () => { + const result = envSchema.safeParse({ ...baseEnv, HOSTING_MODE: 'saas' }) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.HOSTING_MODE).toBe('saas') + } + }) + + it('rejects invalid HOSTING_MODE values', () => { + const result = envSchema.safeParse({ ...baseEnv, HOSTING_MODE: 'cloud' }) + expect(result.success).toBe(false) + }) +}) + describe('parseEnv', () => { it('throws on invalid environment', () => { expect(() => parseEnv({})).toThrow() diff --git a/tests/unit/lib/onboarding-gate.test.ts b/tests/unit/lib/onboarding-gate.test.ts index 29132c3..3ad28dd 100644 --- a/tests/unit/lib/onboarding-gate.test.ts +++ b/tests/unit/lib/onboarding-gate.test.ts @@ -18,6 +18,7 @@ function sampleField(overrides?: Record) { description: null, isMandatory: true, sortOrder: 0, + source: 'admin', config: null, createdAt: new Date(TEST_NOW), updatedAt: new Date(TEST_NOW), @@ -60,12 +61,10 @@ describe('checkOnboardingComplete', () => { resetMocks() }) - it('returns complete=true when community has no onboarding fields', async () => { + it('returns complete=true when community has no mandatory fields', async () => { queueSelectResults( [], // no mandatory fields - [], // no user responses - [], // no community fields at all (no admin age field) - [{ declaredAge: 18 }] // user has declared age + [] // no user responses ) const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) @@ -78,9 +77,7 @@ describe('checkOnboardingComplete', () => { const field = sampleField() queueSelectResults( [field], // mandatory fields - [sampleResponse()], // user responses - [], // no community fields with age_confirmation - [{ declaredAge: 18 }] // user has declared age + [sampleResponse()] // user responses ) const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) @@ -93,9 +90,7 @@ describe('checkOnboardingComplete', () => { const field = sampleField() queueSelectResults( [field], // mandatory fields - [], // no responses - [], // no community fields with age_confirmation - [{ declaredAge: 18 }] // user has declared age (age check passes) + [] // no responses ) const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) @@ -110,12 +105,9 @@ describe('checkOnboardingComplete', () => { const field1 = sampleField({ id: 'field-001', label: 'Intro' }) const field2 = sampleField({ id: 'field-002', label: 'ToS', fieldType: 'tos_acceptance' }) - // Only field-001 answered queueSelectResults( [field1, field2], // mandatory fields - [sampleResponse({ fieldId: 'field-001' })], // only one response - [], // no community fields with age_confirmation - [{ declaredAge: 18 }] // user has declared age + [sampleResponse({ fieldId: 'field-001' })] // only one response ) const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) @@ -126,16 +118,33 @@ describe('checkOnboardingComplete', () => { }) it('only checks mandatory fields (ignores optional)', async () => { - // Only query returns mandatory fields, so optional are not fetched queueSelectResults( - [], // no mandatory fields - [], // no responses - [], // no community fields with age_confirmation - [{ declaredAge: 18 }] // user has declared age + [], // no mandatory fields (optional fields not fetched) + [] // no responses ) const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) expect(result.complete).toBe(true) }) + + it('treats platform fields the same as admin fields', async () => { + const platformField = sampleField({ + id: 'platform:age_confirmation', + source: 'platform', + fieldType: 'age_confirmation', + label: 'Age Declaration', + }) + queueSelectResults( + [platformField], // mandatory platform field + [] // no responses + ) + + const result = await checkOnboardingComplete(mockDb as never, USER_DID, COMMUNITY_DID) + + expect(result.complete).toBe(false) + expect(result.missingFields).toEqual([ + { id: 'platform:age_confirmation', label: 'Age Declaration', fieldType: 'age_confirmation' }, + ]) + }) }) diff --git a/tests/unit/routes/onboarding.test.ts b/tests/unit/routes/onboarding.test.ts index fca7b9a..03eb422 100644 --- a/tests/unit/routes/onboarding.test.ts +++ b/tests/unit/routes/onboarding.test.ts @@ -17,6 +17,7 @@ const COMMUNITY_DID = 'did:plc:community123' const mockEnv = { COMMUNITY_DID, + HOSTING_MODE: 'selfhosted' as const, RATE_LIMIT_WRITE: 10, RATE_LIMIT_READ_ANON: 100, RATE_LIMIT_READ_AUTH: 300, @@ -129,6 +130,7 @@ function sampleField(overrides?: Record) { description: 'Tell us about yourself', isMandatory: true, sortOrder: 0, + source: 'admin', config: null, createdAt: new Date(TEST_NOW), updatedAt: new Date(TEST_NOW), @@ -151,14 +153,14 @@ function sampleResponse(overrides?: Record) { // Helper: build test app // --------------------------------------------------------------------------- -async function buildTestApp(user?: RequestUser): Promise { +async function buildTestApp(user?: RequestUser, envOverrides?: Partial): Promise { const app = Fastify({ logger: false }) const authMiddleware = createMockAuthMiddleware(user) const requireAdmin = createMockRequireAdmin(user) app.decorate('db', mockDb as never) - app.decorate('env', mockEnv) + app.decorate('env', { ...mockEnv, ...envOverrides }) app.decorate('authMiddleware', authMiddleware) app.decorate('requireAdmin', requireAdmin as never) app.decorate('firehose', {} as never) @@ -204,7 +206,7 @@ describe('onboarding admin routes', () => { resetAllDbMocks() }) - it('returns empty array when no fields configured', async () => { + it('returns { fields, hostingMode } response shape', async () => { queueSelectResults([]) const response = await app.inject({ @@ -214,17 +216,20 @@ describe('onboarding admin routes', () => { }) expect(response.statusCode).toBe(200) - expect(response.json()).toEqual([]) + const body = response.json<{ fields: unknown[]; hostingMode: string }>() + expect(body.fields).toEqual([]) + expect(body.hostingMode).toBe('selfhosted') }) - it('returns fields sorted by sortOrder', async () => { + it('returns fields sorted by sortOrder with source property', async () => { const fields = [ - sampleField({ id: 'field-001', sortOrder: 0 }), + sampleField({ id: 'field-001', sortOrder: 0, source: 'admin' }), sampleField({ id: 'field-002', sortOrder: 1, label: 'Accept ToS', fieldType: 'tos_acceptance', + source: 'platform', }), ] queueSelectResults(fields) @@ -236,10 +241,11 @@ describe('onboarding admin routes', () => { }) expect(response.statusCode).toBe(200) - const body = response.json<{ id: string }[]>() - expect(body).toHaveLength(2) - expect(body[0]?.id).toBe('field-001') - expect(body[1]?.id).toBe('field-002') + const body = response.json<{ fields: { id: string; source: string }[]; hostingMode: string }>() + expect(body.fields).toHaveLength(2) + expect(body.fields[0]?.id).toBe('field-001') + expect(body.fields[0]?.source).toBe('admin') + expect(body.fields[1]?.source).toBe('platform') }) it('rejects unauthenticated request', async () => { @@ -444,6 +450,66 @@ describe('onboarding admin routes', () => { expect(response.statusCode).toBe(400) }) + + it('returns 403 when editing platform field in SaaS mode', async () => { + const saasApp = await buildTestApp(adminUser(), { HOSTING_MODE: 'saas' as const }) + // Queue select for the field lookup: platform source + queueSelectResults([sampleField({ id: 'field-001', source: 'platform' })]) + + const response = await saasApp.inject({ + method: 'PUT', + url: '/api/admin/onboarding-fields/field-001', + headers: { + authorization: 'Bearer admin-token', + 'content-type': 'application/json', + }, + payload: { label: 'Modified label' }, + }) + + expect(response.statusCode).toBe(403) + await saasApp.close() + }) + + it('allows editing platform field in selfhosted mode', async () => { + const updated = sampleField({ label: 'Updated label', source: 'platform' }) + const updateChain = createChainableProxy([updated]) + mockDb.update.mockReturnValueOnce(updateChain) + + const response = await app.inject({ + method: 'PUT', + url: '/api/admin/onboarding-fields/field-001', + headers: { + authorization: 'Bearer admin-token', + 'content-type': 'application/json', + }, + payload: { label: 'Updated label' }, + }) + + expect(response.statusCode).toBe(200) + }) + + it('allows editing admin field in SaaS mode', async () => { + const saasApp = await buildTestApp(adminUser(), { HOSTING_MODE: 'saas' as const }) + // Queue select for the field lookup: admin source + queueSelectResults([sampleField({ id: 'field-001', source: 'admin' })]) + // Queue update + const updated = sampleField({ label: 'Updated label', source: 'admin' }) + const updateChain = createChainableProxy([updated]) + mockDb.update.mockReturnValueOnce(updateChain) + + const response = await saasApp.inject({ + method: 'PUT', + url: '/api/admin/onboarding-fields/field-001', + headers: { + authorization: 'Bearer admin-token', + 'content-type': 'application/json', + }, + payload: { label: 'Updated label' }, + }) + + expect(response.statusCode).toBe(200) + await saasApp.close() + }) }) // ========================================================================= @@ -495,6 +561,41 @@ describe('onboarding admin routes', () => { expect(response.statusCode).toBe(404) }) + + it('returns 403 when deleting platform field in SaaS mode', async () => { + const saasApp = await buildTestApp(adminUser(), { HOSTING_MODE: 'saas' as const }) + // Queue select for the field lookup: platform source + queueSelectResults([sampleField({ id: 'field-001', source: 'platform' })]) + + const response = await saasApp.inject({ + method: 'DELETE', + url: '/api/admin/onboarding-fields/field-001', + headers: { authorization: 'Bearer admin-token' }, + }) + + expect(response.statusCode).toBe(403) + await saasApp.close() + }) + + it('allows deleting admin field in SaaS mode', async () => { + const saasApp = await buildTestApp(adminUser(), { HOSTING_MODE: 'saas' as const }) + // Queue select for the field lookup: admin source + queueSelectResults([sampleField({ id: 'field-001', source: 'admin' })]) + // Queue delete + const deleteChain = createChainableProxy([sampleField()]) + mockDb.delete.mockReturnValueOnce(deleteChain) + const deleteResponsesChain = createChainableProxy([]) + mockDb.delete.mockReturnValueOnce(deleteResponsesChain) + + const response = await saasApp.inject({ + method: 'DELETE', + url: '/api/admin/onboarding-fields/field-001', + headers: { authorization: 'Bearer admin-token' }, + }) + + expect(response.statusCode).toBe(200) + await saasApp.close() + }) }) // ========================================================================= @@ -590,8 +691,7 @@ describe('onboarding user routes', () => { it('returns complete=true when no onboarding fields exist', async () => { queueSelectResults( [], // fields - [], // responses - [{ declaredAge: 18 }] // user preferences (has declared age) + [] // responses ) const response = await app.inject({ @@ -610,8 +710,7 @@ describe('onboarding user routes', () => { const field = sampleField({ isMandatory: true }) queueSelectResults( [field], // fields - [], // no responses - [{ declaredAge: 18 }] // user preferences (has declared age) + [] // no responses ) const response = await app.inject({ @@ -630,8 +729,7 @@ describe('onboarding user routes', () => { const field = sampleField({ isMandatory: true }) queueSelectResults( [field], // fields - [sampleResponse()], // responses - [{ declaredAge: 18 }] // user preferences (has declared age) + [sampleResponse()] // responses ) const response = await app.inject({ @@ -646,6 +744,24 @@ describe('onboarding user routes', () => { expect(body.fields[0]?.completed).toBe(true) }) + it('includes source in response fields', async () => { + const field = sampleField({ source: 'platform' }) + queueSelectResults( + [field], // fields + [sampleResponse()] // responses + ) + + const response = await app.inject({ + method: 'GET', + url: '/api/onboarding/status', + headers: { authorization: 'Bearer user-token' }, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ fields: { source: string }[] }>() + expect(body.fields[0]?.source).toBe('platform') + }) + it('ignores optional fields for completeness check', async () => { const mandatoryField = sampleField({ id: 'field-001', isMandatory: true }) const optionalField = sampleField({ @@ -658,8 +774,7 @@ describe('onboarding user routes', () => { // Only mandatory field answered queueSelectResults( [mandatoryField, optionalField], - [sampleResponse({ fieldId: 'field-001' })], - [{ declaredAge: 18 }] // user preferences (has declared age) + [sampleResponse({ fieldId: 'field-001' })] ) const response = await app.inject({ diff --git a/tests/unit/setup/service.test.ts b/tests/unit/setup/service.test.ts index 13f5844..58d5da7 100644 --- a/tests/unit/setup/service.test.ts +++ b/tests/unit/setup/service.test.ts @@ -20,14 +20,17 @@ function createMockDb() { }) // Upsert chain: db.insert().values().onConflictDoUpdate().returning() -> Promise + // Also supports: db.insert().values().onConflictDoNothing() -> Promise const returningFn = vi.fn<() => Promise>() const onConflictDoUpdateFn = vi.fn<() => { returning: typeof returningFn }>().mockReturnValue({ returning: returningFn, }) + const onConflictDoNothingFn = vi.fn<() => Promise>().mockResolvedValue([]) const valuesFn = vi - .fn<() => { onConflictDoUpdate: typeof onConflictDoUpdateFn }>() + .fn<() => { onConflictDoUpdate: typeof onConflictDoUpdateFn; onConflictDoNothing: typeof onConflictDoNothingFn }>() .mockReturnValue({ onConflictDoUpdate: onConflictDoUpdateFn, + onConflictDoNothing: onConflictDoNothingFn, }) const insertFn = vi.fn<() => { values: typeof valuesFn }>().mockReturnValue({ values: valuesFn, @@ -51,6 +54,7 @@ function createMockDb() { insertFn, valuesFn, onConflictDoUpdateFn, + onConflictDoNothingFn, returningFn, updateFn, setFn, @@ -295,6 +299,36 @@ describe('SetupService', () => { expect(result).toStrictEqual({ alreadyInitialized: true }) expect(mocks.updateFn).not.toHaveBeenCalled() }) + + it('seeds platform:age_confirmation onboarding field after initialization', async () => { + mocks.returningFn.mockResolvedValueOnce([ + { communityName: DEFAULT_COMMUNITY_NAME, communityDid: TEST_COMMUNITY_DID }, + ]) + + await service.initialize({ did: TEST_DID, communityDid: TEST_COMMUNITY_DID }) + + // The insert should be called twice: once for community settings, once for onboarding field + expect(mocks.insertFn).toHaveBeenCalledTimes(2) + + // The second insert's values call should contain the platform age field + const secondValuesCall = mocks.valuesFn.mock.calls[1]?.[0] as Record + expect(secondValuesCall).toBeDefined() + expect(secondValuesCall.id).toBe('platform:age_confirmation') + expect(secondValuesCall.fieldType).toBe('age_confirmation') + expect(secondValuesCall.source).toBe('platform') + expect(secondValuesCall.isMandatory).toBe(true) + expect(secondValuesCall.sortOrder).toBe(-1) + expect(mocks.onConflictDoNothingFn).toHaveBeenCalled() + }) + + it('does not seed platform fields when community is already initialized', async () => { + mocks.returningFn.mockResolvedValueOnce([]) + + await service.initialize({ did: TEST_DID }) + + // Only one insert call (the upsert attempt), no seeding + expect(mocks.insertFn).toHaveBeenCalledTimes(1) + }) }) // =========================================================================