Barazo AppView backend barazo.forum

feat(admin): add design upload endpoints for logo and favicon (#123)

* feat(admin): add design upload endpoints for logo and favicon

Add dedicated admin design routes for logo (512x512, WebP q85) and
favicon (256x256, WebP q90) uploads. Add faviconUrl column to
community_settings schema with migration. Extend admin-settings
route and validation to support faviconUrl in GET/PUT endpoints
and public settings response.

* fix(admin): regenerate migration snapshot and fix test lint issues

Regenerated the favicon_url migration via drizzle-kit to include the
required snapshot file for CI schema drift check. Also removed an
unused eslint-disable directive and fixed the mock requireAdmin
preHandler to use sync done() callback instead of async.

* fix(test): add favicon_url column to tenant isolation schema

The integration test creates tables manually via pushSchema() rather
than running migrations. Add the new favicon_url column to keep the
hardcoded schema in sync with the Drizzle schema.

authored by

Guido X Jansen and committed by
GitHub
ab5ae9ec 22ed942c

+4472 -1
+1
drizzle/0001_add_favicon_url.sql
··· 1 + ALTER TABLE "community_settings" ADD COLUMN "favicon_url" text;
+3835
drizzle/meta/0001_snapshot.json
··· 1 + { 2 + "id": "162ade1e-0657-4007-8e90-3bef3a52306c", 3 + "prevId": "021aacdb-67eb-4c57-93aa-7f7166a8e364", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.users": { 8 + "name": "users", 9 + "schema": "", 10 + "columns": { 11 + "did": { 12 + "name": "did", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "handle": { 18 + "name": "handle", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "display_name": { 24 + "name": "display_name", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": false 28 + }, 29 + "avatar_url": { 30 + "name": "avatar_url", 31 + "type": "text", 32 + "primaryKey": false, 33 + "notNull": false 34 + }, 35 + "banner_url": { 36 + "name": "banner_url", 37 + "type": "text", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "bio": { 42 + "name": "bio", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": false 46 + }, 47 + "role": { 48 + "name": "role", 49 + "type": "text", 50 + "primaryKey": false, 51 + "notNull": true, 52 + "default": "'user'" 53 + }, 54 + "is_banned": { 55 + "name": "is_banned", 56 + "type": "boolean", 57 + "primaryKey": false, 58 + "notNull": true, 59 + "default": false 60 + }, 61 + "reputation_score": { 62 + "name": "reputation_score", 63 + "type": "integer", 64 + "primaryKey": false, 65 + "notNull": true, 66 + "default": 0 67 + }, 68 + "first_seen_at": { 69 + "name": "first_seen_at", 70 + "type": "timestamp with time zone", 71 + "primaryKey": false, 72 + "notNull": true, 73 + "default": "now()" 74 + }, 75 + "last_active_at": { 76 + "name": "last_active_at", 77 + "type": "timestamp with time zone", 78 + "primaryKey": false, 79 + "notNull": true, 80 + "default": "now()" 81 + }, 82 + "declared_age": { 83 + "name": "declared_age", 84 + "type": "integer", 85 + "primaryKey": false, 86 + "notNull": false 87 + }, 88 + "maturity_pref": { 89 + "name": "maturity_pref", 90 + "type": "text", 91 + "primaryKey": false, 92 + "notNull": true, 93 + "default": "'safe'" 94 + }, 95 + "account_created_at": { 96 + "name": "account_created_at", 97 + "type": "timestamp with time zone", 98 + "primaryKey": false, 99 + "notNull": false 100 + }, 101 + "followers_count": { 102 + "name": "followers_count", 103 + "type": "integer", 104 + "primaryKey": false, 105 + "notNull": true, 106 + "default": 0 107 + }, 108 + "follows_count": { 109 + "name": "follows_count", 110 + "type": "integer", 111 + "primaryKey": false, 112 + "notNull": true, 113 + "default": 0 114 + }, 115 + "atproto_posts_count": { 116 + "name": "atproto_posts_count", 117 + "type": "integer", 118 + "primaryKey": false, 119 + "notNull": true, 120 + "default": 0 121 + }, 122 + "has_bluesky_profile": { 123 + "name": "has_bluesky_profile", 124 + "type": "boolean", 125 + "primaryKey": false, 126 + "notNull": true, 127 + "default": false 128 + }, 129 + "atproto_labels": { 130 + "name": "atproto_labels", 131 + "type": "jsonb", 132 + "primaryKey": false, 133 + "notNull": true, 134 + "default": "'[]'::jsonb" 135 + } 136 + }, 137 + "indexes": { 138 + "users_role_elevated_idx": { 139 + "name": "users_role_elevated_idx", 140 + "columns": [ 141 + { 142 + "expression": "role", 143 + "isExpression": false, 144 + "asc": true, 145 + "nulls": "last" 146 + } 147 + ], 148 + "isUnique": false, 149 + "where": "role IN ('moderator', 'admin')", 150 + "concurrently": false, 151 + "method": "btree", 152 + "with": {} 153 + }, 154 + "users_handle_idx": { 155 + "name": "users_handle_idx", 156 + "columns": [ 157 + { 158 + "expression": "handle", 159 + "isExpression": false, 160 + "asc": true, 161 + "nulls": "last" 162 + } 163 + ], 164 + "isUnique": false, 165 + "concurrently": false, 166 + "method": "btree", 167 + "with": {} 168 + }, 169 + "users_account_created_at_idx": { 170 + "name": "users_account_created_at_idx", 171 + "columns": [ 172 + { 173 + "expression": "account_created_at", 174 + "isExpression": false, 175 + "asc": true, 176 + "nulls": "last" 177 + } 178 + ], 179 + "isUnique": false, 180 + "concurrently": false, 181 + "method": "btree", 182 + "with": {} 183 + } 184 + }, 185 + "foreignKeys": {}, 186 + "compositePrimaryKeys": {}, 187 + "uniqueConstraints": {}, 188 + "policies": {}, 189 + "checkConstraints": {}, 190 + "isRLSEnabled": false 191 + }, 192 + "public.firehose_cursor": { 193 + "name": "firehose_cursor", 194 + "schema": "", 195 + "columns": { 196 + "id": { 197 + "name": "id", 198 + "type": "text", 199 + "primaryKey": true, 200 + "notNull": true, 201 + "default": "'default'" 202 + }, 203 + "cursor": { 204 + "name": "cursor", 205 + "type": "bigint", 206 + "primaryKey": false, 207 + "notNull": false 208 + }, 209 + "updated_at": { 210 + "name": "updated_at", 211 + "type": "timestamp with time zone", 212 + "primaryKey": false, 213 + "notNull": true, 214 + "default": "now()" 215 + } 216 + }, 217 + "indexes": {}, 218 + "foreignKeys": {}, 219 + "compositePrimaryKeys": {}, 220 + "uniqueConstraints": {}, 221 + "policies": {}, 222 + "checkConstraints": {}, 223 + "isRLSEnabled": false 224 + }, 225 + "public.topics": { 226 + "name": "topics", 227 + "schema": "", 228 + "columns": { 229 + "uri": { 230 + "name": "uri", 231 + "type": "text", 232 + "primaryKey": true, 233 + "notNull": true 234 + }, 235 + "rkey": { 236 + "name": "rkey", 237 + "type": "text", 238 + "primaryKey": false, 239 + "notNull": true 240 + }, 241 + "author_did": { 242 + "name": "author_did", 243 + "type": "text", 244 + "primaryKey": false, 245 + "notNull": true 246 + }, 247 + "title": { 248 + "name": "title", 249 + "type": "text", 250 + "primaryKey": false, 251 + "notNull": true 252 + }, 253 + "content": { 254 + "name": "content", 255 + "type": "text", 256 + "primaryKey": false, 257 + "notNull": true 258 + }, 259 + "content_format": { 260 + "name": "content_format", 261 + "type": "text", 262 + "primaryKey": false, 263 + "notNull": false 264 + }, 265 + "category": { 266 + "name": "category", 267 + "type": "text", 268 + "primaryKey": false, 269 + "notNull": true 270 + }, 271 + "tags": { 272 + "name": "tags", 273 + "type": "jsonb", 274 + "primaryKey": false, 275 + "notNull": false 276 + }, 277 + "community_did": { 278 + "name": "community_did", 279 + "type": "text", 280 + "primaryKey": false, 281 + "notNull": true 282 + }, 283 + "cid": { 284 + "name": "cid", 285 + "type": "text", 286 + "primaryKey": false, 287 + "notNull": true 288 + }, 289 + "labels": { 290 + "name": "labels", 291 + "type": "jsonb", 292 + "primaryKey": false, 293 + "notNull": false 294 + }, 295 + "reply_count": { 296 + "name": "reply_count", 297 + "type": "integer", 298 + "primaryKey": false, 299 + "notNull": true, 300 + "default": 0 301 + }, 302 + "reaction_count": { 303 + "name": "reaction_count", 304 + "type": "integer", 305 + "primaryKey": false, 306 + "notNull": true, 307 + "default": 0 308 + }, 309 + "vote_count": { 310 + "name": "vote_count", 311 + "type": "integer", 312 + "primaryKey": false, 313 + "notNull": true, 314 + "default": 0 315 + }, 316 + "last_activity_at": { 317 + "name": "last_activity_at", 318 + "type": "timestamp with time zone", 319 + "primaryKey": false, 320 + "notNull": true, 321 + "default": "now()" 322 + }, 323 + "created_at": { 324 + "name": "created_at", 325 + "type": "timestamp with time zone", 326 + "primaryKey": false, 327 + "notNull": true 328 + }, 329 + "indexed_at": { 330 + "name": "indexed_at", 331 + "type": "timestamp with time zone", 332 + "primaryKey": false, 333 + "notNull": true, 334 + "default": "now()" 335 + }, 336 + "is_locked": { 337 + "name": "is_locked", 338 + "type": "boolean", 339 + "primaryKey": false, 340 + "notNull": true, 341 + "default": false 342 + }, 343 + "is_pinned": { 344 + "name": "is_pinned", 345 + "type": "boolean", 346 + "primaryKey": false, 347 + "notNull": true, 348 + "default": false 349 + }, 350 + "is_mod_deleted": { 351 + "name": "is_mod_deleted", 352 + "type": "boolean", 353 + "primaryKey": false, 354 + "notNull": true, 355 + "default": false 356 + }, 357 + "is_author_deleted": { 358 + "name": "is_author_deleted", 359 + "type": "boolean", 360 + "primaryKey": false, 361 + "notNull": true, 362 + "default": false 363 + }, 364 + "moderation_status": { 365 + "name": "moderation_status", 366 + "type": "text", 367 + "primaryKey": false, 368 + "notNull": true, 369 + "default": "'approved'" 370 + }, 371 + "trust_status": { 372 + "name": "trust_status", 373 + "type": "text", 374 + "primaryKey": false, 375 + "notNull": true, 376 + "default": "'trusted'" 377 + } 378 + }, 379 + "indexes": { 380 + "topics_author_did_idx": { 381 + "name": "topics_author_did_idx", 382 + "columns": [ 383 + { 384 + "expression": "author_did", 385 + "isExpression": false, 386 + "asc": true, 387 + "nulls": "last" 388 + } 389 + ], 390 + "isUnique": false, 391 + "concurrently": false, 392 + "method": "btree", 393 + "with": {} 394 + }, 395 + "topics_category_idx": { 396 + "name": "topics_category_idx", 397 + "columns": [ 398 + { 399 + "expression": "category", 400 + "isExpression": false, 401 + "asc": true, 402 + "nulls": "last" 403 + } 404 + ], 405 + "isUnique": false, 406 + "concurrently": false, 407 + "method": "btree", 408 + "with": {} 409 + }, 410 + "topics_created_at_idx": { 411 + "name": "topics_created_at_idx", 412 + "columns": [ 413 + { 414 + "expression": "created_at", 415 + "isExpression": false, 416 + "asc": true, 417 + "nulls": "last" 418 + } 419 + ], 420 + "isUnique": false, 421 + "concurrently": false, 422 + "method": "btree", 423 + "with": {} 424 + }, 425 + "topics_last_activity_at_idx": { 426 + "name": "topics_last_activity_at_idx", 427 + "columns": [ 428 + { 429 + "expression": "last_activity_at", 430 + "isExpression": false, 431 + "asc": true, 432 + "nulls": "last" 433 + } 434 + ], 435 + "isUnique": false, 436 + "concurrently": false, 437 + "method": "btree", 438 + "with": {} 439 + }, 440 + "topics_community_did_idx": { 441 + "name": "topics_community_did_idx", 442 + "columns": [ 443 + { 444 + "expression": "community_did", 445 + "isExpression": false, 446 + "asc": true, 447 + "nulls": "last" 448 + } 449 + ], 450 + "isUnique": false, 451 + "concurrently": false, 452 + "method": "btree", 453 + "with": {} 454 + }, 455 + "topics_moderation_status_idx": { 456 + "name": "topics_moderation_status_idx", 457 + "columns": [ 458 + { 459 + "expression": "moderation_status", 460 + "isExpression": false, 461 + "asc": true, 462 + "nulls": "last" 463 + } 464 + ], 465 + "isUnique": false, 466 + "concurrently": false, 467 + "method": "btree", 468 + "with": {} 469 + }, 470 + "topics_trust_status_idx": { 471 + "name": "topics_trust_status_idx", 472 + "columns": [ 473 + { 474 + "expression": "trust_status", 475 + "isExpression": false, 476 + "asc": true, 477 + "nulls": "last" 478 + } 479 + ], 480 + "isUnique": false, 481 + "concurrently": false, 482 + "method": "btree", 483 + "with": {} 484 + }, 485 + "topics_community_category_activity_idx": { 486 + "name": "topics_community_category_activity_idx", 487 + "columns": [ 488 + { 489 + "expression": "community_did", 490 + "isExpression": false, 491 + "asc": true, 492 + "nulls": "last" 493 + }, 494 + { 495 + "expression": "category", 496 + "isExpression": false, 497 + "asc": true, 498 + "nulls": "last" 499 + }, 500 + { 501 + "expression": "last_activity_at", 502 + "isExpression": false, 503 + "asc": true, 504 + "nulls": "last" 505 + } 506 + ], 507 + "isUnique": false, 508 + "concurrently": false, 509 + "method": "btree", 510 + "with": {} 511 + } 512 + }, 513 + "foreignKeys": {}, 514 + "compositePrimaryKeys": {}, 515 + "uniqueConstraints": {}, 516 + "policies": { 517 + "tenant_isolation": { 518 + "name": "tenant_isolation", 519 + "as": "PERMISSIVE", 520 + "for": "ALL", 521 + "to": [ 522 + "barazo_app" 523 + ], 524 + "using": "community_did = current_setting('app.current_community_did', true)", 525 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 526 + } 527 + }, 528 + "checkConstraints": {}, 529 + "isRLSEnabled": true 530 + }, 531 + "public.replies": { 532 + "name": "replies", 533 + "schema": "", 534 + "columns": { 535 + "uri": { 536 + "name": "uri", 537 + "type": "text", 538 + "primaryKey": true, 539 + "notNull": true 540 + }, 541 + "rkey": { 542 + "name": "rkey", 543 + "type": "text", 544 + "primaryKey": false, 545 + "notNull": true 546 + }, 547 + "author_did": { 548 + "name": "author_did", 549 + "type": "text", 550 + "primaryKey": false, 551 + "notNull": true 552 + }, 553 + "content": { 554 + "name": "content", 555 + "type": "text", 556 + "primaryKey": false, 557 + "notNull": true 558 + }, 559 + "content_format": { 560 + "name": "content_format", 561 + "type": "text", 562 + "primaryKey": false, 563 + "notNull": false 564 + }, 565 + "root_uri": { 566 + "name": "root_uri", 567 + "type": "text", 568 + "primaryKey": false, 569 + "notNull": true 570 + }, 571 + "root_cid": { 572 + "name": "root_cid", 573 + "type": "text", 574 + "primaryKey": false, 575 + "notNull": true 576 + }, 577 + "parent_uri": { 578 + "name": "parent_uri", 579 + "type": "text", 580 + "primaryKey": false, 581 + "notNull": true 582 + }, 583 + "parent_cid": { 584 + "name": "parent_cid", 585 + "type": "text", 586 + "primaryKey": false, 587 + "notNull": true 588 + }, 589 + "community_did": { 590 + "name": "community_did", 591 + "type": "text", 592 + "primaryKey": false, 593 + "notNull": true 594 + }, 595 + "cid": { 596 + "name": "cid", 597 + "type": "text", 598 + "primaryKey": false, 599 + "notNull": true 600 + }, 601 + "labels": { 602 + "name": "labels", 603 + "type": "jsonb", 604 + "primaryKey": false, 605 + "notNull": false 606 + }, 607 + "reaction_count": { 608 + "name": "reaction_count", 609 + "type": "integer", 610 + "primaryKey": false, 611 + "notNull": true, 612 + "default": 0 613 + }, 614 + "vote_count": { 615 + "name": "vote_count", 616 + "type": "integer", 617 + "primaryKey": false, 618 + "notNull": true, 619 + "default": 0 620 + }, 621 + "created_at": { 622 + "name": "created_at", 623 + "type": "timestamp with time zone", 624 + "primaryKey": false, 625 + "notNull": true 626 + }, 627 + "indexed_at": { 628 + "name": "indexed_at", 629 + "type": "timestamp with time zone", 630 + "primaryKey": false, 631 + "notNull": true, 632 + "default": "now()" 633 + }, 634 + "is_author_deleted": { 635 + "name": "is_author_deleted", 636 + "type": "boolean", 637 + "primaryKey": false, 638 + "notNull": true, 639 + "default": false 640 + }, 641 + "is_mod_deleted": { 642 + "name": "is_mod_deleted", 643 + "type": "boolean", 644 + "primaryKey": false, 645 + "notNull": true, 646 + "default": false 647 + }, 648 + "moderation_status": { 649 + "name": "moderation_status", 650 + "type": "text", 651 + "primaryKey": false, 652 + "notNull": true, 653 + "default": "'approved'" 654 + }, 655 + "trust_status": { 656 + "name": "trust_status", 657 + "type": "text", 658 + "primaryKey": false, 659 + "notNull": true, 660 + "default": "'trusted'" 661 + } 662 + }, 663 + "indexes": { 664 + "replies_author_did_idx": { 665 + "name": "replies_author_did_idx", 666 + "columns": [ 667 + { 668 + "expression": "author_did", 669 + "isExpression": false, 670 + "asc": true, 671 + "nulls": "last" 672 + } 673 + ], 674 + "isUnique": false, 675 + "concurrently": false, 676 + "method": "btree", 677 + "with": {} 678 + }, 679 + "replies_root_uri_idx": { 680 + "name": "replies_root_uri_idx", 681 + "columns": [ 682 + { 683 + "expression": "root_uri", 684 + "isExpression": false, 685 + "asc": true, 686 + "nulls": "last" 687 + } 688 + ], 689 + "isUnique": false, 690 + "concurrently": false, 691 + "method": "btree", 692 + "with": {} 693 + }, 694 + "replies_parent_uri_idx": { 695 + "name": "replies_parent_uri_idx", 696 + "columns": [ 697 + { 698 + "expression": "parent_uri", 699 + "isExpression": false, 700 + "asc": true, 701 + "nulls": "last" 702 + } 703 + ], 704 + "isUnique": false, 705 + "concurrently": false, 706 + "method": "btree", 707 + "with": {} 708 + }, 709 + "replies_created_at_idx": { 710 + "name": "replies_created_at_idx", 711 + "columns": [ 712 + { 713 + "expression": "created_at", 714 + "isExpression": false, 715 + "asc": true, 716 + "nulls": "last" 717 + } 718 + ], 719 + "isUnique": false, 720 + "concurrently": false, 721 + "method": "btree", 722 + "with": {} 723 + }, 724 + "replies_community_did_idx": { 725 + "name": "replies_community_did_idx", 726 + "columns": [ 727 + { 728 + "expression": "community_did", 729 + "isExpression": false, 730 + "asc": true, 731 + "nulls": "last" 732 + } 733 + ], 734 + "isUnique": false, 735 + "concurrently": false, 736 + "method": "btree", 737 + "with": {} 738 + }, 739 + "replies_moderation_status_idx": { 740 + "name": "replies_moderation_status_idx", 741 + "columns": [ 742 + { 743 + "expression": "moderation_status", 744 + "isExpression": false, 745 + "asc": true, 746 + "nulls": "last" 747 + } 748 + ], 749 + "isUnique": false, 750 + "concurrently": false, 751 + "method": "btree", 752 + "with": {} 753 + }, 754 + "replies_trust_status_idx": { 755 + "name": "replies_trust_status_idx", 756 + "columns": [ 757 + { 758 + "expression": "trust_status", 759 + "isExpression": false, 760 + "asc": true, 761 + "nulls": "last" 762 + } 763 + ], 764 + "isUnique": false, 765 + "concurrently": false, 766 + "method": "btree", 767 + "with": {} 768 + }, 769 + "replies_root_uri_created_at_idx": { 770 + "name": "replies_root_uri_created_at_idx", 771 + "columns": [ 772 + { 773 + "expression": "root_uri", 774 + "isExpression": false, 775 + "asc": true, 776 + "nulls": "last" 777 + }, 778 + { 779 + "expression": "created_at", 780 + "isExpression": false, 781 + "asc": true, 782 + "nulls": "last" 783 + } 784 + ], 785 + "isUnique": false, 786 + "concurrently": false, 787 + "method": "btree", 788 + "with": {} 789 + } 790 + }, 791 + "foreignKeys": {}, 792 + "compositePrimaryKeys": {}, 793 + "uniqueConstraints": {}, 794 + "policies": { 795 + "tenant_isolation": { 796 + "name": "tenant_isolation", 797 + "as": "PERMISSIVE", 798 + "for": "ALL", 799 + "to": [ 800 + "barazo_app" 801 + ], 802 + "using": "community_did = current_setting('app.current_community_did', true)", 803 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 804 + } 805 + }, 806 + "checkConstraints": {}, 807 + "isRLSEnabled": true 808 + }, 809 + "public.reactions": { 810 + "name": "reactions", 811 + "schema": "", 812 + "columns": { 813 + "uri": { 814 + "name": "uri", 815 + "type": "text", 816 + "primaryKey": true, 817 + "notNull": true 818 + }, 819 + "rkey": { 820 + "name": "rkey", 821 + "type": "text", 822 + "primaryKey": false, 823 + "notNull": true 824 + }, 825 + "author_did": { 826 + "name": "author_did", 827 + "type": "text", 828 + "primaryKey": false, 829 + "notNull": true 830 + }, 831 + "subject_uri": { 832 + "name": "subject_uri", 833 + "type": "text", 834 + "primaryKey": false, 835 + "notNull": true 836 + }, 837 + "subject_cid": { 838 + "name": "subject_cid", 839 + "type": "text", 840 + "primaryKey": false, 841 + "notNull": true 842 + }, 843 + "type": { 844 + "name": "type", 845 + "type": "text", 846 + "primaryKey": false, 847 + "notNull": true 848 + }, 849 + "community_did": { 850 + "name": "community_did", 851 + "type": "text", 852 + "primaryKey": false, 853 + "notNull": true 854 + }, 855 + "cid": { 856 + "name": "cid", 857 + "type": "text", 858 + "primaryKey": false, 859 + "notNull": true 860 + }, 861 + "created_at": { 862 + "name": "created_at", 863 + "type": "timestamp with time zone", 864 + "primaryKey": false, 865 + "notNull": true 866 + }, 867 + "indexed_at": { 868 + "name": "indexed_at", 869 + "type": "timestamp with time zone", 870 + "primaryKey": false, 871 + "notNull": true, 872 + "default": "now()" 873 + } 874 + }, 875 + "indexes": { 876 + "reactions_author_did_idx": { 877 + "name": "reactions_author_did_idx", 878 + "columns": [ 879 + { 880 + "expression": "author_did", 881 + "isExpression": false, 882 + "asc": true, 883 + "nulls": "last" 884 + } 885 + ], 886 + "isUnique": false, 887 + "concurrently": false, 888 + "method": "btree", 889 + "with": {} 890 + }, 891 + "reactions_subject_uri_idx": { 892 + "name": "reactions_subject_uri_idx", 893 + "columns": [ 894 + { 895 + "expression": "subject_uri", 896 + "isExpression": false, 897 + "asc": true, 898 + "nulls": "last" 899 + } 900 + ], 901 + "isUnique": false, 902 + "concurrently": false, 903 + "method": "btree", 904 + "with": {} 905 + }, 906 + "reactions_community_did_idx": { 907 + "name": "reactions_community_did_idx", 908 + "columns": [ 909 + { 910 + "expression": "community_did", 911 + "isExpression": false, 912 + "asc": true, 913 + "nulls": "last" 914 + } 915 + ], 916 + "isUnique": false, 917 + "concurrently": false, 918 + "method": "btree", 919 + "with": {} 920 + }, 921 + "reactions_subject_uri_type_idx": { 922 + "name": "reactions_subject_uri_type_idx", 923 + "columns": [ 924 + { 925 + "expression": "subject_uri", 926 + "isExpression": false, 927 + "asc": true, 928 + "nulls": "last" 929 + }, 930 + { 931 + "expression": "type", 932 + "isExpression": false, 933 + "asc": true, 934 + "nulls": "last" 935 + } 936 + ], 937 + "isUnique": false, 938 + "concurrently": false, 939 + "method": "btree", 940 + "with": {} 941 + } 942 + }, 943 + "foreignKeys": {}, 944 + "compositePrimaryKeys": {}, 945 + "uniqueConstraints": { 946 + "reactions_author_subject_type_uniq": { 947 + "name": "reactions_author_subject_type_uniq", 948 + "nullsNotDistinct": false, 949 + "columns": [ 950 + "author_did", 951 + "subject_uri", 952 + "type" 953 + ] 954 + } 955 + }, 956 + "policies": { 957 + "tenant_isolation": { 958 + "name": "tenant_isolation", 959 + "as": "PERMISSIVE", 960 + "for": "ALL", 961 + "to": [ 962 + "barazo_app" 963 + ], 964 + "using": "community_did = current_setting('app.current_community_did', true)", 965 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 966 + } 967 + }, 968 + "checkConstraints": {}, 969 + "isRLSEnabled": true 970 + }, 971 + "public.votes": { 972 + "name": "votes", 973 + "schema": "", 974 + "columns": { 975 + "uri": { 976 + "name": "uri", 977 + "type": "text", 978 + "primaryKey": true, 979 + "notNull": true 980 + }, 981 + "rkey": { 982 + "name": "rkey", 983 + "type": "text", 984 + "primaryKey": false, 985 + "notNull": true 986 + }, 987 + "author_did": { 988 + "name": "author_did", 989 + "type": "text", 990 + "primaryKey": false, 991 + "notNull": true 992 + }, 993 + "subject_uri": { 994 + "name": "subject_uri", 995 + "type": "text", 996 + "primaryKey": false, 997 + "notNull": true 998 + }, 999 + "subject_cid": { 1000 + "name": "subject_cid", 1001 + "type": "text", 1002 + "primaryKey": false, 1003 + "notNull": true 1004 + }, 1005 + "direction": { 1006 + "name": "direction", 1007 + "type": "text", 1008 + "primaryKey": false, 1009 + "notNull": true 1010 + }, 1011 + "community_did": { 1012 + "name": "community_did", 1013 + "type": "text", 1014 + "primaryKey": false, 1015 + "notNull": true 1016 + }, 1017 + "cid": { 1018 + "name": "cid", 1019 + "type": "text", 1020 + "primaryKey": false, 1021 + "notNull": true 1022 + }, 1023 + "created_at": { 1024 + "name": "created_at", 1025 + "type": "timestamp with time zone", 1026 + "primaryKey": false, 1027 + "notNull": true 1028 + }, 1029 + "indexed_at": { 1030 + "name": "indexed_at", 1031 + "type": "timestamp with time zone", 1032 + "primaryKey": false, 1033 + "notNull": true, 1034 + "default": "now()" 1035 + } 1036 + }, 1037 + "indexes": { 1038 + "votes_author_did_idx": { 1039 + "name": "votes_author_did_idx", 1040 + "columns": [ 1041 + { 1042 + "expression": "author_did", 1043 + "isExpression": false, 1044 + "asc": true, 1045 + "nulls": "last" 1046 + } 1047 + ], 1048 + "isUnique": false, 1049 + "concurrently": false, 1050 + "method": "btree", 1051 + "with": {} 1052 + }, 1053 + "votes_subject_uri_idx": { 1054 + "name": "votes_subject_uri_idx", 1055 + "columns": [ 1056 + { 1057 + "expression": "subject_uri", 1058 + "isExpression": false, 1059 + "asc": true, 1060 + "nulls": "last" 1061 + } 1062 + ], 1063 + "isUnique": false, 1064 + "concurrently": false, 1065 + "method": "btree", 1066 + "with": {} 1067 + }, 1068 + "votes_community_did_idx": { 1069 + "name": "votes_community_did_idx", 1070 + "columns": [ 1071 + { 1072 + "expression": "community_did", 1073 + "isExpression": false, 1074 + "asc": true, 1075 + "nulls": "last" 1076 + } 1077 + ], 1078 + "isUnique": false, 1079 + "concurrently": false, 1080 + "method": "btree", 1081 + "with": {} 1082 + } 1083 + }, 1084 + "foreignKeys": {}, 1085 + "compositePrimaryKeys": {}, 1086 + "uniqueConstraints": { 1087 + "votes_author_subject_uniq": { 1088 + "name": "votes_author_subject_uniq", 1089 + "nullsNotDistinct": false, 1090 + "columns": [ 1091 + "author_did", 1092 + "subject_uri" 1093 + ] 1094 + } 1095 + }, 1096 + "policies": {}, 1097 + "checkConstraints": {}, 1098 + "isRLSEnabled": false 1099 + }, 1100 + "public.tracked_repos": { 1101 + "name": "tracked_repos", 1102 + "schema": "", 1103 + "columns": { 1104 + "did": { 1105 + "name": "did", 1106 + "type": "text", 1107 + "primaryKey": true, 1108 + "notNull": true 1109 + }, 1110 + "tracked_at": { 1111 + "name": "tracked_at", 1112 + "type": "timestamp with time zone", 1113 + "primaryKey": false, 1114 + "notNull": true, 1115 + "default": "now()" 1116 + } 1117 + }, 1118 + "indexes": {}, 1119 + "foreignKeys": {}, 1120 + "compositePrimaryKeys": {}, 1121 + "uniqueConstraints": {}, 1122 + "policies": {}, 1123 + "checkConstraints": {}, 1124 + "isRLSEnabled": false 1125 + }, 1126 + "public.community_settings": { 1127 + "name": "community_settings", 1128 + "schema": "", 1129 + "columns": { 1130 + "community_did": { 1131 + "name": "community_did", 1132 + "type": "text", 1133 + "primaryKey": true, 1134 + "notNull": true 1135 + }, 1136 + "domains": { 1137 + "name": "domains", 1138 + "type": "jsonb", 1139 + "primaryKey": false, 1140 + "notNull": true, 1141 + "default": "'[]'::jsonb" 1142 + }, 1143 + "initialized": { 1144 + "name": "initialized", 1145 + "type": "boolean", 1146 + "primaryKey": false, 1147 + "notNull": true, 1148 + "default": false 1149 + }, 1150 + "admin_did": { 1151 + "name": "admin_did", 1152 + "type": "text", 1153 + "primaryKey": false, 1154 + "notNull": false 1155 + }, 1156 + "community_name": { 1157 + "name": "community_name", 1158 + "type": "text", 1159 + "primaryKey": false, 1160 + "notNull": true, 1161 + "default": "'Barazo Community'" 1162 + }, 1163 + "maturity_rating": { 1164 + "name": "maturity_rating", 1165 + "type": "text", 1166 + "primaryKey": false, 1167 + "notNull": true, 1168 + "default": "'safe'" 1169 + }, 1170 + "reaction_set": { 1171 + "name": "reaction_set", 1172 + "type": "jsonb", 1173 + "primaryKey": false, 1174 + "notNull": true, 1175 + "default": "'[\"like\"]'::jsonb" 1176 + }, 1177 + "moderation_thresholds": { 1178 + "name": "moderation_thresholds", 1179 + "type": "jsonb", 1180 + "primaryKey": false, 1181 + "notNull": true, 1182 + "default": "'{\"autoBlockReportCount\":5,\"warnThreshold\":3,\"firstPostQueueCount\":3,\"newAccountDays\":7,\"newAccountWriteRatePerMin\":3,\"establishedWriteRatePerMin\":10,\"linkHoldEnabled\":true,\"topicCreationDelayEnabled\":true,\"burstPostCount\":5,\"burstWindowMinutes\":10,\"trustedPostThreshold\":10}'::jsonb" 1183 + }, 1184 + "word_filter": { 1185 + "name": "word_filter", 1186 + "type": "jsonb", 1187 + "primaryKey": false, 1188 + "notNull": true, 1189 + "default": "'[]'::jsonb" 1190 + }, 1191 + "jurisdiction_country": { 1192 + "name": "jurisdiction_country", 1193 + "type": "text", 1194 + "primaryKey": false, 1195 + "notNull": false 1196 + }, 1197 + "age_threshold": { 1198 + "name": "age_threshold", 1199 + "type": "integer", 1200 + "primaryKey": false, 1201 + "notNull": true, 1202 + "default": 16 1203 + }, 1204 + "require_login_for_mature": { 1205 + "name": "require_login_for_mature", 1206 + "type": "boolean", 1207 + "primaryKey": false, 1208 + "notNull": true, 1209 + "default": true 1210 + }, 1211 + "community_description": { 1212 + "name": "community_description", 1213 + "type": "text", 1214 + "primaryKey": false, 1215 + "notNull": false 1216 + }, 1217 + "handle": { 1218 + "name": "handle", 1219 + "type": "text", 1220 + "primaryKey": false, 1221 + "notNull": false 1222 + }, 1223 + "service_endpoint": { 1224 + "name": "service_endpoint", 1225 + "type": "text", 1226 + "primaryKey": false, 1227 + "notNull": false 1228 + }, 1229 + "signing_key": { 1230 + "name": "signing_key", 1231 + "type": "text", 1232 + "primaryKey": false, 1233 + "notNull": false 1234 + }, 1235 + "rotation_key": { 1236 + "name": "rotation_key", 1237 + "type": "text", 1238 + "primaryKey": false, 1239 + "notNull": false 1240 + }, 1241 + "community_logo_url": { 1242 + "name": "community_logo_url", 1243 + "type": "text", 1244 + "primaryKey": false, 1245 + "notNull": false 1246 + }, 1247 + "favicon_url": { 1248 + "name": "favicon_url", 1249 + "type": "text", 1250 + "primaryKey": false, 1251 + "notNull": false 1252 + }, 1253 + "primary_color": { 1254 + "name": "primary_color", 1255 + "type": "text", 1256 + "primaryKey": false, 1257 + "notNull": false 1258 + }, 1259 + "accent_color": { 1260 + "name": "accent_color", 1261 + "type": "text", 1262 + "primaryKey": false, 1263 + "notNull": false 1264 + }, 1265 + "created_at": { 1266 + "name": "created_at", 1267 + "type": "timestamp with time zone", 1268 + "primaryKey": false, 1269 + "notNull": true, 1270 + "default": "now()" 1271 + }, 1272 + "updated_at": { 1273 + "name": "updated_at", 1274 + "type": "timestamp with time zone", 1275 + "primaryKey": false, 1276 + "notNull": true, 1277 + "default": "now()" 1278 + } 1279 + }, 1280 + "indexes": {}, 1281 + "foreignKeys": {}, 1282 + "compositePrimaryKeys": {}, 1283 + "uniqueConstraints": {}, 1284 + "policies": { 1285 + "tenant_isolation": { 1286 + "name": "tenant_isolation", 1287 + "as": "PERMISSIVE", 1288 + "for": "ALL", 1289 + "to": [ 1290 + "barazo_app" 1291 + ], 1292 + "using": "community_did = current_setting('app.current_community_did', true)", 1293 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1294 + } 1295 + }, 1296 + "checkConstraints": {}, 1297 + "isRLSEnabled": true 1298 + }, 1299 + "public.categories": { 1300 + "name": "categories", 1301 + "schema": "", 1302 + "columns": { 1303 + "id": { 1304 + "name": "id", 1305 + "type": "text", 1306 + "primaryKey": true, 1307 + "notNull": true 1308 + }, 1309 + "slug": { 1310 + "name": "slug", 1311 + "type": "text", 1312 + "primaryKey": false, 1313 + "notNull": true 1314 + }, 1315 + "name": { 1316 + "name": "name", 1317 + "type": "text", 1318 + "primaryKey": false, 1319 + "notNull": true 1320 + }, 1321 + "description": { 1322 + "name": "description", 1323 + "type": "text", 1324 + "primaryKey": false, 1325 + "notNull": false 1326 + }, 1327 + "parent_id": { 1328 + "name": "parent_id", 1329 + "type": "text", 1330 + "primaryKey": false, 1331 + "notNull": false 1332 + }, 1333 + "sort_order": { 1334 + "name": "sort_order", 1335 + "type": "integer", 1336 + "primaryKey": false, 1337 + "notNull": true, 1338 + "default": 0 1339 + }, 1340 + "community_did": { 1341 + "name": "community_did", 1342 + "type": "text", 1343 + "primaryKey": false, 1344 + "notNull": true 1345 + }, 1346 + "maturity_rating": { 1347 + "name": "maturity_rating", 1348 + "type": "text", 1349 + "primaryKey": false, 1350 + "notNull": true, 1351 + "default": "'safe'" 1352 + }, 1353 + "created_at": { 1354 + "name": "created_at", 1355 + "type": "timestamp with time zone", 1356 + "primaryKey": false, 1357 + "notNull": true, 1358 + "default": "now()" 1359 + }, 1360 + "updated_at": { 1361 + "name": "updated_at", 1362 + "type": "timestamp with time zone", 1363 + "primaryKey": false, 1364 + "notNull": true, 1365 + "default": "now()" 1366 + } 1367 + }, 1368 + "indexes": { 1369 + "categories_slug_community_did_idx": { 1370 + "name": "categories_slug_community_did_idx", 1371 + "columns": [ 1372 + { 1373 + "expression": "slug", 1374 + "isExpression": false, 1375 + "asc": true, 1376 + "nulls": "last" 1377 + }, 1378 + { 1379 + "expression": "community_did", 1380 + "isExpression": false, 1381 + "asc": true, 1382 + "nulls": "last" 1383 + } 1384 + ], 1385 + "isUnique": true, 1386 + "concurrently": false, 1387 + "method": "btree", 1388 + "with": {} 1389 + }, 1390 + "categories_parent_id_idx": { 1391 + "name": "categories_parent_id_idx", 1392 + "columns": [ 1393 + { 1394 + "expression": "parent_id", 1395 + "isExpression": false, 1396 + "asc": true, 1397 + "nulls": "last" 1398 + } 1399 + ], 1400 + "isUnique": false, 1401 + "concurrently": false, 1402 + "method": "btree", 1403 + "with": {} 1404 + }, 1405 + "categories_community_did_idx": { 1406 + "name": "categories_community_did_idx", 1407 + "columns": [ 1408 + { 1409 + "expression": "community_did", 1410 + "isExpression": false, 1411 + "asc": true, 1412 + "nulls": "last" 1413 + } 1414 + ], 1415 + "isUnique": false, 1416 + "concurrently": false, 1417 + "method": "btree", 1418 + "with": {} 1419 + }, 1420 + "categories_maturity_rating_idx": { 1421 + "name": "categories_maturity_rating_idx", 1422 + "columns": [ 1423 + { 1424 + "expression": "maturity_rating", 1425 + "isExpression": false, 1426 + "asc": true, 1427 + "nulls": "last" 1428 + } 1429 + ], 1430 + "isUnique": false, 1431 + "concurrently": false, 1432 + "method": "btree", 1433 + "with": {} 1434 + } 1435 + }, 1436 + "foreignKeys": { 1437 + "categories_parent_id_fk": { 1438 + "name": "categories_parent_id_fk", 1439 + "tableFrom": "categories", 1440 + "tableTo": "categories", 1441 + "columnsFrom": [ 1442 + "parent_id" 1443 + ], 1444 + "columnsTo": [ 1445 + "id" 1446 + ], 1447 + "onDelete": "set null", 1448 + "onUpdate": "no action" 1449 + } 1450 + }, 1451 + "compositePrimaryKeys": {}, 1452 + "uniqueConstraints": {}, 1453 + "policies": { 1454 + "tenant_isolation": { 1455 + "name": "tenant_isolation", 1456 + "as": "PERMISSIVE", 1457 + "for": "ALL", 1458 + "to": [ 1459 + "barazo_app" 1460 + ], 1461 + "using": "community_did = current_setting('app.current_community_did', true)", 1462 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1463 + } 1464 + }, 1465 + "checkConstraints": {}, 1466 + "isRLSEnabled": true 1467 + }, 1468 + "public.moderation_actions": { 1469 + "name": "moderation_actions", 1470 + "schema": "", 1471 + "columns": { 1472 + "id": { 1473 + "name": "id", 1474 + "type": "serial", 1475 + "primaryKey": true, 1476 + "notNull": true 1477 + }, 1478 + "action": { 1479 + "name": "action", 1480 + "type": "text", 1481 + "primaryKey": false, 1482 + "notNull": true 1483 + }, 1484 + "target_uri": { 1485 + "name": "target_uri", 1486 + "type": "text", 1487 + "primaryKey": false, 1488 + "notNull": false 1489 + }, 1490 + "target_did": { 1491 + "name": "target_did", 1492 + "type": "text", 1493 + "primaryKey": false, 1494 + "notNull": false 1495 + }, 1496 + "moderator_did": { 1497 + "name": "moderator_did", 1498 + "type": "text", 1499 + "primaryKey": false, 1500 + "notNull": true 1501 + }, 1502 + "community_did": { 1503 + "name": "community_did", 1504 + "type": "text", 1505 + "primaryKey": false, 1506 + "notNull": true 1507 + }, 1508 + "reason": { 1509 + "name": "reason", 1510 + "type": "text", 1511 + "primaryKey": false, 1512 + "notNull": false 1513 + }, 1514 + "created_at": { 1515 + "name": "created_at", 1516 + "type": "timestamp with time zone", 1517 + "primaryKey": false, 1518 + "notNull": true, 1519 + "default": "now()" 1520 + } 1521 + }, 1522 + "indexes": { 1523 + "mod_actions_moderator_did_idx": { 1524 + "name": "mod_actions_moderator_did_idx", 1525 + "columns": [ 1526 + { 1527 + "expression": "moderator_did", 1528 + "isExpression": false, 1529 + "asc": true, 1530 + "nulls": "last" 1531 + } 1532 + ], 1533 + "isUnique": false, 1534 + "concurrently": false, 1535 + "method": "btree", 1536 + "with": {} 1537 + }, 1538 + "mod_actions_community_did_idx": { 1539 + "name": "mod_actions_community_did_idx", 1540 + "columns": [ 1541 + { 1542 + "expression": "community_did", 1543 + "isExpression": false, 1544 + "asc": true, 1545 + "nulls": "last" 1546 + } 1547 + ], 1548 + "isUnique": false, 1549 + "concurrently": false, 1550 + "method": "btree", 1551 + "with": {} 1552 + }, 1553 + "mod_actions_created_at_idx": { 1554 + "name": "mod_actions_created_at_idx", 1555 + "columns": [ 1556 + { 1557 + "expression": "created_at", 1558 + "isExpression": false, 1559 + "asc": true, 1560 + "nulls": "last" 1561 + } 1562 + ], 1563 + "isUnique": false, 1564 + "concurrently": false, 1565 + "method": "btree", 1566 + "with": {} 1567 + }, 1568 + "mod_actions_target_uri_idx": { 1569 + "name": "mod_actions_target_uri_idx", 1570 + "columns": [ 1571 + { 1572 + "expression": "target_uri", 1573 + "isExpression": false, 1574 + "asc": true, 1575 + "nulls": "last" 1576 + } 1577 + ], 1578 + "isUnique": false, 1579 + "concurrently": false, 1580 + "method": "btree", 1581 + "with": {} 1582 + }, 1583 + "mod_actions_target_did_idx": { 1584 + "name": "mod_actions_target_did_idx", 1585 + "columns": [ 1586 + { 1587 + "expression": "target_did", 1588 + "isExpression": false, 1589 + "asc": true, 1590 + "nulls": "last" 1591 + } 1592 + ], 1593 + "isUnique": false, 1594 + "concurrently": false, 1595 + "method": "btree", 1596 + "with": {} 1597 + } 1598 + }, 1599 + "foreignKeys": {}, 1600 + "compositePrimaryKeys": {}, 1601 + "uniqueConstraints": {}, 1602 + "policies": { 1603 + "tenant_isolation": { 1604 + "name": "tenant_isolation", 1605 + "as": "PERMISSIVE", 1606 + "for": "ALL", 1607 + "to": [ 1608 + "barazo_app" 1609 + ], 1610 + "using": "community_did = current_setting('app.current_community_did', true)", 1611 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1612 + } 1613 + }, 1614 + "checkConstraints": {}, 1615 + "isRLSEnabled": true 1616 + }, 1617 + "public.reports": { 1618 + "name": "reports", 1619 + "schema": "", 1620 + "columns": { 1621 + "id": { 1622 + "name": "id", 1623 + "type": "serial", 1624 + "primaryKey": true, 1625 + "notNull": true 1626 + }, 1627 + "reporter_did": { 1628 + "name": "reporter_did", 1629 + "type": "text", 1630 + "primaryKey": false, 1631 + "notNull": true 1632 + }, 1633 + "target_uri": { 1634 + "name": "target_uri", 1635 + "type": "text", 1636 + "primaryKey": false, 1637 + "notNull": true 1638 + }, 1639 + "target_did": { 1640 + "name": "target_did", 1641 + "type": "text", 1642 + "primaryKey": false, 1643 + "notNull": true 1644 + }, 1645 + "reason_type": { 1646 + "name": "reason_type", 1647 + "type": "text", 1648 + "primaryKey": false, 1649 + "notNull": true 1650 + }, 1651 + "description": { 1652 + "name": "description", 1653 + "type": "text", 1654 + "primaryKey": false, 1655 + "notNull": false 1656 + }, 1657 + "community_did": { 1658 + "name": "community_did", 1659 + "type": "text", 1660 + "primaryKey": false, 1661 + "notNull": true 1662 + }, 1663 + "status": { 1664 + "name": "status", 1665 + "type": "text", 1666 + "primaryKey": false, 1667 + "notNull": true, 1668 + "default": "'pending'" 1669 + }, 1670 + "resolution_type": { 1671 + "name": "resolution_type", 1672 + "type": "text", 1673 + "primaryKey": false, 1674 + "notNull": false 1675 + }, 1676 + "resolved_by": { 1677 + "name": "resolved_by", 1678 + "type": "text", 1679 + "primaryKey": false, 1680 + "notNull": false 1681 + }, 1682 + "resolved_at": { 1683 + "name": "resolved_at", 1684 + "type": "timestamp with time zone", 1685 + "primaryKey": false, 1686 + "notNull": false 1687 + }, 1688 + "appeal_reason": { 1689 + "name": "appeal_reason", 1690 + "type": "text", 1691 + "primaryKey": false, 1692 + "notNull": false 1693 + }, 1694 + "appealed_at": { 1695 + "name": "appealed_at", 1696 + "type": "timestamp with time zone", 1697 + "primaryKey": false, 1698 + "notNull": false 1699 + }, 1700 + "appeal_status": { 1701 + "name": "appeal_status", 1702 + "type": "text", 1703 + "primaryKey": false, 1704 + "notNull": true, 1705 + "default": "'none'" 1706 + }, 1707 + "created_at": { 1708 + "name": "created_at", 1709 + "type": "timestamp with time zone", 1710 + "primaryKey": false, 1711 + "notNull": true, 1712 + "default": "now()" 1713 + } 1714 + }, 1715 + "indexes": { 1716 + "reports_reporter_did_idx": { 1717 + "name": "reports_reporter_did_idx", 1718 + "columns": [ 1719 + { 1720 + "expression": "reporter_did", 1721 + "isExpression": false, 1722 + "asc": true, 1723 + "nulls": "last" 1724 + } 1725 + ], 1726 + "isUnique": false, 1727 + "concurrently": false, 1728 + "method": "btree", 1729 + "with": {} 1730 + }, 1731 + "reports_target_uri_idx": { 1732 + "name": "reports_target_uri_idx", 1733 + "columns": [ 1734 + { 1735 + "expression": "target_uri", 1736 + "isExpression": false, 1737 + "asc": true, 1738 + "nulls": "last" 1739 + } 1740 + ], 1741 + "isUnique": false, 1742 + "concurrently": false, 1743 + "method": "btree", 1744 + "with": {} 1745 + }, 1746 + "reports_target_did_idx": { 1747 + "name": "reports_target_did_idx", 1748 + "columns": [ 1749 + { 1750 + "expression": "target_did", 1751 + "isExpression": false, 1752 + "asc": true, 1753 + "nulls": "last" 1754 + } 1755 + ], 1756 + "isUnique": false, 1757 + "concurrently": false, 1758 + "method": "btree", 1759 + "with": {} 1760 + }, 1761 + "reports_community_did_idx": { 1762 + "name": "reports_community_did_idx", 1763 + "columns": [ 1764 + { 1765 + "expression": "community_did", 1766 + "isExpression": false, 1767 + "asc": true, 1768 + "nulls": "last" 1769 + } 1770 + ], 1771 + "isUnique": false, 1772 + "concurrently": false, 1773 + "method": "btree", 1774 + "with": {} 1775 + }, 1776 + "reports_status_idx": { 1777 + "name": "reports_status_idx", 1778 + "columns": [ 1779 + { 1780 + "expression": "status", 1781 + "isExpression": false, 1782 + "asc": true, 1783 + "nulls": "last" 1784 + } 1785 + ], 1786 + "isUnique": false, 1787 + "concurrently": false, 1788 + "method": "btree", 1789 + "with": {} 1790 + }, 1791 + "reports_created_at_idx": { 1792 + "name": "reports_created_at_idx", 1793 + "columns": [ 1794 + { 1795 + "expression": "created_at", 1796 + "isExpression": false, 1797 + "asc": true, 1798 + "nulls": "last" 1799 + } 1800 + ], 1801 + "isUnique": false, 1802 + "concurrently": false, 1803 + "method": "btree", 1804 + "with": {} 1805 + }, 1806 + "reports_unique_reporter_target_idx": { 1807 + "name": "reports_unique_reporter_target_idx", 1808 + "columns": [ 1809 + { 1810 + "expression": "reporter_did", 1811 + "isExpression": false, 1812 + "asc": true, 1813 + "nulls": "last" 1814 + }, 1815 + { 1816 + "expression": "target_uri", 1817 + "isExpression": false, 1818 + "asc": true, 1819 + "nulls": "last" 1820 + }, 1821 + { 1822 + "expression": "community_did", 1823 + "isExpression": false, 1824 + "asc": true, 1825 + "nulls": "last" 1826 + } 1827 + ], 1828 + "isUnique": true, 1829 + "concurrently": false, 1830 + "method": "btree", 1831 + "with": {} 1832 + } 1833 + }, 1834 + "foreignKeys": {}, 1835 + "compositePrimaryKeys": {}, 1836 + "uniqueConstraints": {}, 1837 + "policies": { 1838 + "tenant_isolation": { 1839 + "name": "tenant_isolation", 1840 + "as": "PERMISSIVE", 1841 + "for": "ALL", 1842 + "to": [ 1843 + "barazo_app" 1844 + ], 1845 + "using": "community_did = current_setting('app.current_community_did', true)", 1846 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1847 + } 1848 + }, 1849 + "checkConstraints": {}, 1850 + "isRLSEnabled": true 1851 + }, 1852 + "public.notifications": { 1853 + "name": "notifications", 1854 + "schema": "", 1855 + "columns": { 1856 + "id": { 1857 + "name": "id", 1858 + "type": "serial", 1859 + "primaryKey": true, 1860 + "notNull": true 1861 + }, 1862 + "recipient_did": { 1863 + "name": "recipient_did", 1864 + "type": "text", 1865 + "primaryKey": false, 1866 + "notNull": true 1867 + }, 1868 + "type": { 1869 + "name": "type", 1870 + "type": "text", 1871 + "primaryKey": false, 1872 + "notNull": true 1873 + }, 1874 + "subject_uri": { 1875 + "name": "subject_uri", 1876 + "type": "text", 1877 + "primaryKey": false, 1878 + "notNull": true 1879 + }, 1880 + "actor_did": { 1881 + "name": "actor_did", 1882 + "type": "text", 1883 + "primaryKey": false, 1884 + "notNull": true 1885 + }, 1886 + "community_did": { 1887 + "name": "community_did", 1888 + "type": "text", 1889 + "primaryKey": false, 1890 + "notNull": true 1891 + }, 1892 + "read": { 1893 + "name": "read", 1894 + "type": "boolean", 1895 + "primaryKey": false, 1896 + "notNull": true, 1897 + "default": false 1898 + }, 1899 + "created_at": { 1900 + "name": "created_at", 1901 + "type": "timestamp with time zone", 1902 + "primaryKey": false, 1903 + "notNull": true, 1904 + "default": "now()" 1905 + } 1906 + }, 1907 + "indexes": { 1908 + "notifications_recipient_did_idx": { 1909 + "name": "notifications_recipient_did_idx", 1910 + "columns": [ 1911 + { 1912 + "expression": "recipient_did", 1913 + "isExpression": false, 1914 + "asc": true, 1915 + "nulls": "last" 1916 + } 1917 + ], 1918 + "isUnique": false, 1919 + "concurrently": false, 1920 + "method": "btree", 1921 + "with": {} 1922 + }, 1923 + "notifications_recipient_read_idx": { 1924 + "name": "notifications_recipient_read_idx", 1925 + "columns": [ 1926 + { 1927 + "expression": "recipient_did", 1928 + "isExpression": false, 1929 + "asc": true, 1930 + "nulls": "last" 1931 + }, 1932 + { 1933 + "expression": "read", 1934 + "isExpression": false, 1935 + "asc": true, 1936 + "nulls": "last" 1937 + } 1938 + ], 1939 + "isUnique": false, 1940 + "concurrently": false, 1941 + "method": "btree", 1942 + "with": {} 1943 + }, 1944 + "notifications_created_at_idx": { 1945 + "name": "notifications_created_at_idx", 1946 + "columns": [ 1947 + { 1948 + "expression": "created_at", 1949 + "isExpression": false, 1950 + "asc": true, 1951 + "nulls": "last" 1952 + } 1953 + ], 1954 + "isUnique": false, 1955 + "concurrently": false, 1956 + "method": "btree", 1957 + "with": {} 1958 + } 1959 + }, 1960 + "foreignKeys": {}, 1961 + "compositePrimaryKeys": {}, 1962 + "uniqueConstraints": {}, 1963 + "policies": { 1964 + "tenant_isolation": { 1965 + "name": "tenant_isolation", 1966 + "as": "PERMISSIVE", 1967 + "for": "ALL", 1968 + "to": [ 1969 + "barazo_app" 1970 + ], 1971 + "using": "community_did = current_setting('app.current_community_did', true)", 1972 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1973 + } 1974 + }, 1975 + "checkConstraints": {}, 1976 + "isRLSEnabled": true 1977 + }, 1978 + "public.user_community_preferences": { 1979 + "name": "user_community_preferences", 1980 + "schema": "", 1981 + "columns": { 1982 + "did": { 1983 + "name": "did", 1984 + "type": "text", 1985 + "primaryKey": false, 1986 + "notNull": true 1987 + }, 1988 + "community_did": { 1989 + "name": "community_did", 1990 + "type": "text", 1991 + "primaryKey": false, 1992 + "notNull": true 1993 + }, 1994 + "maturity_override": { 1995 + "name": "maturity_override", 1996 + "type": "text", 1997 + "primaryKey": false, 1998 + "notNull": false 1999 + }, 2000 + "muted_words": { 2001 + "name": "muted_words", 2002 + "type": "jsonb", 2003 + "primaryKey": false, 2004 + "notNull": false 2005 + }, 2006 + "blocked_dids": { 2007 + "name": "blocked_dids", 2008 + "type": "jsonb", 2009 + "primaryKey": false, 2010 + "notNull": false 2011 + }, 2012 + "muted_dids": { 2013 + "name": "muted_dids", 2014 + "type": "jsonb", 2015 + "primaryKey": false, 2016 + "notNull": false 2017 + }, 2018 + "notification_prefs": { 2019 + "name": "notification_prefs", 2020 + "type": "jsonb", 2021 + "primaryKey": false, 2022 + "notNull": false 2023 + }, 2024 + "updated_at": { 2025 + "name": "updated_at", 2026 + "type": "timestamp with time zone", 2027 + "primaryKey": false, 2028 + "notNull": true, 2029 + "default": "now()" 2030 + } 2031 + }, 2032 + "indexes": { 2033 + "user_community_prefs_did_idx": { 2034 + "name": "user_community_prefs_did_idx", 2035 + "columns": [ 2036 + { 2037 + "expression": "did", 2038 + "isExpression": false, 2039 + "asc": true, 2040 + "nulls": "last" 2041 + } 2042 + ], 2043 + "isUnique": false, 2044 + "concurrently": false, 2045 + "method": "btree", 2046 + "with": {} 2047 + }, 2048 + "user_community_prefs_community_idx": { 2049 + "name": "user_community_prefs_community_idx", 2050 + "columns": [ 2051 + { 2052 + "expression": "community_did", 2053 + "isExpression": false, 2054 + "asc": true, 2055 + "nulls": "last" 2056 + } 2057 + ], 2058 + "isUnique": false, 2059 + "concurrently": false, 2060 + "method": "btree", 2061 + "with": {} 2062 + } 2063 + }, 2064 + "foreignKeys": {}, 2065 + "compositePrimaryKeys": { 2066 + "user_community_preferences_did_community_did_pk": { 2067 + "name": "user_community_preferences_did_community_did_pk", 2068 + "columns": [ 2069 + "did", 2070 + "community_did" 2071 + ] 2072 + } 2073 + }, 2074 + "uniqueConstraints": {}, 2075 + "policies": { 2076 + "tenant_isolation": { 2077 + "name": "tenant_isolation", 2078 + "as": "PERMISSIVE", 2079 + "for": "ALL", 2080 + "to": [ 2081 + "barazo_app" 2082 + ], 2083 + "using": "community_did = current_setting('app.current_community_did', true)", 2084 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2085 + } 2086 + }, 2087 + "checkConstraints": {}, 2088 + "isRLSEnabled": true 2089 + }, 2090 + "public.user_preferences": { 2091 + "name": "user_preferences", 2092 + "schema": "", 2093 + "columns": { 2094 + "did": { 2095 + "name": "did", 2096 + "type": "text", 2097 + "primaryKey": true, 2098 + "notNull": true 2099 + }, 2100 + "maturity_level": { 2101 + "name": "maturity_level", 2102 + "type": "text", 2103 + "primaryKey": false, 2104 + "notNull": true, 2105 + "default": "'sfw'" 2106 + }, 2107 + "declared_age": { 2108 + "name": "declared_age", 2109 + "type": "integer", 2110 + "primaryKey": false, 2111 + "notNull": false 2112 + }, 2113 + "muted_words": { 2114 + "name": "muted_words", 2115 + "type": "jsonb", 2116 + "primaryKey": false, 2117 + "notNull": true, 2118 + "default": "'[]'::jsonb" 2119 + }, 2120 + "blocked_dids": { 2121 + "name": "blocked_dids", 2122 + "type": "jsonb", 2123 + "primaryKey": false, 2124 + "notNull": true, 2125 + "default": "'[]'::jsonb" 2126 + }, 2127 + "muted_dids": { 2128 + "name": "muted_dids", 2129 + "type": "jsonb", 2130 + "primaryKey": false, 2131 + "notNull": true, 2132 + "default": "'[]'::jsonb" 2133 + }, 2134 + "cross_post_bluesky": { 2135 + "name": "cross_post_bluesky", 2136 + "type": "boolean", 2137 + "primaryKey": false, 2138 + "notNull": true, 2139 + "default": false 2140 + }, 2141 + "cross_post_frontpage": { 2142 + "name": "cross_post_frontpage", 2143 + "type": "boolean", 2144 + "primaryKey": false, 2145 + "notNull": true, 2146 + "default": false 2147 + }, 2148 + "cross_post_scopes_granted": { 2149 + "name": "cross_post_scopes_granted", 2150 + "type": "boolean", 2151 + "primaryKey": false, 2152 + "notNull": true, 2153 + "default": false 2154 + }, 2155 + "updated_at": { 2156 + "name": "updated_at", 2157 + "type": "timestamp with time zone", 2158 + "primaryKey": false, 2159 + "notNull": true, 2160 + "default": "now()" 2161 + } 2162 + }, 2163 + "indexes": {}, 2164 + "foreignKeys": {}, 2165 + "compositePrimaryKeys": {}, 2166 + "uniqueConstraints": {}, 2167 + "policies": {}, 2168 + "checkConstraints": {}, 2169 + "isRLSEnabled": false 2170 + }, 2171 + "public.cross_posts": { 2172 + "name": "cross_posts", 2173 + "schema": "", 2174 + "columns": { 2175 + "id": { 2176 + "name": "id", 2177 + "type": "text", 2178 + "primaryKey": true, 2179 + "notNull": true 2180 + }, 2181 + "topic_uri": { 2182 + "name": "topic_uri", 2183 + "type": "text", 2184 + "primaryKey": false, 2185 + "notNull": true 2186 + }, 2187 + "service": { 2188 + "name": "service", 2189 + "type": "text", 2190 + "primaryKey": false, 2191 + "notNull": true 2192 + }, 2193 + "cross_post_uri": { 2194 + "name": "cross_post_uri", 2195 + "type": "text", 2196 + "primaryKey": false, 2197 + "notNull": true 2198 + }, 2199 + "cross_post_cid": { 2200 + "name": "cross_post_cid", 2201 + "type": "text", 2202 + "primaryKey": false, 2203 + "notNull": true 2204 + }, 2205 + "author_did": { 2206 + "name": "author_did", 2207 + "type": "text", 2208 + "primaryKey": false, 2209 + "notNull": true 2210 + }, 2211 + "created_at": { 2212 + "name": "created_at", 2213 + "type": "timestamp with time zone", 2214 + "primaryKey": false, 2215 + "notNull": true, 2216 + "default": "now()" 2217 + } 2218 + }, 2219 + "indexes": { 2220 + "cross_posts_topic_uri_idx": { 2221 + "name": "cross_posts_topic_uri_idx", 2222 + "columns": [ 2223 + { 2224 + "expression": "topic_uri", 2225 + "isExpression": false, 2226 + "asc": true, 2227 + "nulls": "last" 2228 + } 2229 + ], 2230 + "isUnique": false, 2231 + "concurrently": false, 2232 + "method": "btree", 2233 + "with": {} 2234 + }, 2235 + "cross_posts_author_did_idx": { 2236 + "name": "cross_posts_author_did_idx", 2237 + "columns": [ 2238 + { 2239 + "expression": "author_did", 2240 + "isExpression": false, 2241 + "asc": true, 2242 + "nulls": "last" 2243 + } 2244 + ], 2245 + "isUnique": false, 2246 + "concurrently": false, 2247 + "method": "btree", 2248 + "with": {} 2249 + } 2250 + }, 2251 + "foreignKeys": {}, 2252 + "compositePrimaryKeys": {}, 2253 + "uniqueConstraints": {}, 2254 + "policies": {}, 2255 + "checkConstraints": {}, 2256 + "isRLSEnabled": false 2257 + }, 2258 + "public.community_onboarding_fields": { 2259 + "name": "community_onboarding_fields", 2260 + "schema": "", 2261 + "columns": { 2262 + "id": { 2263 + "name": "id", 2264 + "type": "text", 2265 + "primaryKey": true, 2266 + "notNull": true 2267 + }, 2268 + "community_did": { 2269 + "name": "community_did", 2270 + "type": "text", 2271 + "primaryKey": false, 2272 + "notNull": true 2273 + }, 2274 + "field_type": { 2275 + "name": "field_type", 2276 + "type": "text", 2277 + "primaryKey": false, 2278 + "notNull": true 2279 + }, 2280 + "label": { 2281 + "name": "label", 2282 + "type": "text", 2283 + "primaryKey": false, 2284 + "notNull": true 2285 + }, 2286 + "description": { 2287 + "name": "description", 2288 + "type": "text", 2289 + "primaryKey": false, 2290 + "notNull": false 2291 + }, 2292 + "is_mandatory": { 2293 + "name": "is_mandatory", 2294 + "type": "boolean", 2295 + "primaryKey": false, 2296 + "notNull": true, 2297 + "default": true 2298 + }, 2299 + "sort_order": { 2300 + "name": "sort_order", 2301 + "type": "integer", 2302 + "primaryKey": false, 2303 + "notNull": true, 2304 + "default": 0 2305 + }, 2306 + "config": { 2307 + "name": "config", 2308 + "type": "jsonb", 2309 + "primaryKey": false, 2310 + "notNull": false 2311 + }, 2312 + "created_at": { 2313 + "name": "created_at", 2314 + "type": "timestamp with time zone", 2315 + "primaryKey": false, 2316 + "notNull": true, 2317 + "default": "now()" 2318 + }, 2319 + "updated_at": { 2320 + "name": "updated_at", 2321 + "type": "timestamp with time zone", 2322 + "primaryKey": false, 2323 + "notNull": true, 2324 + "default": "now()" 2325 + } 2326 + }, 2327 + "indexes": { 2328 + "onboarding_fields_community_idx": { 2329 + "name": "onboarding_fields_community_idx", 2330 + "columns": [ 2331 + { 2332 + "expression": "community_did", 2333 + "isExpression": false, 2334 + "asc": true, 2335 + "nulls": "last" 2336 + } 2337 + ], 2338 + "isUnique": false, 2339 + "concurrently": false, 2340 + "method": "btree", 2341 + "with": {} 2342 + } 2343 + }, 2344 + "foreignKeys": {}, 2345 + "compositePrimaryKeys": {}, 2346 + "uniqueConstraints": {}, 2347 + "policies": { 2348 + "tenant_isolation": { 2349 + "name": "tenant_isolation", 2350 + "as": "PERMISSIVE", 2351 + "for": "ALL", 2352 + "to": [ 2353 + "barazo_app" 2354 + ], 2355 + "using": "community_did = current_setting('app.current_community_did', true)", 2356 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2357 + } 2358 + }, 2359 + "checkConstraints": {}, 2360 + "isRLSEnabled": true 2361 + }, 2362 + "public.user_onboarding_responses": { 2363 + "name": "user_onboarding_responses", 2364 + "schema": "", 2365 + "columns": { 2366 + "did": { 2367 + "name": "did", 2368 + "type": "text", 2369 + "primaryKey": false, 2370 + "notNull": true 2371 + }, 2372 + "community_did": { 2373 + "name": "community_did", 2374 + "type": "text", 2375 + "primaryKey": false, 2376 + "notNull": true 2377 + }, 2378 + "field_id": { 2379 + "name": "field_id", 2380 + "type": "text", 2381 + "primaryKey": false, 2382 + "notNull": true 2383 + }, 2384 + "response": { 2385 + "name": "response", 2386 + "type": "jsonb", 2387 + "primaryKey": false, 2388 + "notNull": true 2389 + }, 2390 + "completed_at": { 2391 + "name": "completed_at", 2392 + "type": "timestamp with time zone", 2393 + "primaryKey": false, 2394 + "notNull": true, 2395 + "default": "now()" 2396 + } 2397 + }, 2398 + "indexes": { 2399 + "onboarding_responses_did_community_idx": { 2400 + "name": "onboarding_responses_did_community_idx", 2401 + "columns": [ 2402 + { 2403 + "expression": "did", 2404 + "isExpression": false, 2405 + "asc": true, 2406 + "nulls": "last" 2407 + }, 2408 + { 2409 + "expression": "community_did", 2410 + "isExpression": false, 2411 + "asc": true, 2412 + "nulls": "last" 2413 + } 2414 + ], 2415 + "isUnique": false, 2416 + "concurrently": false, 2417 + "method": "btree", 2418 + "with": {} 2419 + } 2420 + }, 2421 + "foreignKeys": {}, 2422 + "compositePrimaryKeys": { 2423 + "user_onboarding_responses_did_community_did_field_id_pk": { 2424 + "name": "user_onboarding_responses_did_community_did_field_id_pk", 2425 + "columns": [ 2426 + "did", 2427 + "community_did", 2428 + "field_id" 2429 + ] 2430 + } 2431 + }, 2432 + "uniqueConstraints": {}, 2433 + "policies": { 2434 + "tenant_isolation": { 2435 + "name": "tenant_isolation", 2436 + "as": "PERMISSIVE", 2437 + "for": "ALL", 2438 + "to": [ 2439 + "barazo_app" 2440 + ], 2441 + "using": "community_did = current_setting('app.current_community_did', true)", 2442 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2443 + } 2444 + }, 2445 + "checkConstraints": {}, 2446 + "isRLSEnabled": true 2447 + }, 2448 + "public.moderation_queue": { 2449 + "name": "moderation_queue", 2450 + "schema": "", 2451 + "columns": { 2452 + "id": { 2453 + "name": "id", 2454 + "type": "serial", 2455 + "primaryKey": true, 2456 + "notNull": true 2457 + }, 2458 + "content_uri": { 2459 + "name": "content_uri", 2460 + "type": "text", 2461 + "primaryKey": false, 2462 + "notNull": true 2463 + }, 2464 + "content_type": { 2465 + "name": "content_type", 2466 + "type": "text", 2467 + "primaryKey": false, 2468 + "notNull": true 2469 + }, 2470 + "author_did": { 2471 + "name": "author_did", 2472 + "type": "text", 2473 + "primaryKey": false, 2474 + "notNull": true 2475 + }, 2476 + "community_did": { 2477 + "name": "community_did", 2478 + "type": "text", 2479 + "primaryKey": false, 2480 + "notNull": true 2481 + }, 2482 + "queue_reason": { 2483 + "name": "queue_reason", 2484 + "type": "text", 2485 + "primaryKey": false, 2486 + "notNull": true 2487 + }, 2488 + "matched_words": { 2489 + "name": "matched_words", 2490 + "type": "jsonb", 2491 + "primaryKey": false, 2492 + "notNull": false 2493 + }, 2494 + "status": { 2495 + "name": "status", 2496 + "type": "text", 2497 + "primaryKey": false, 2498 + "notNull": true, 2499 + "default": "'pending'" 2500 + }, 2501 + "reviewed_by": { 2502 + "name": "reviewed_by", 2503 + "type": "text", 2504 + "primaryKey": false, 2505 + "notNull": false 2506 + }, 2507 + "created_at": { 2508 + "name": "created_at", 2509 + "type": "timestamp with time zone", 2510 + "primaryKey": false, 2511 + "notNull": true, 2512 + "default": "now()" 2513 + }, 2514 + "reviewed_at": { 2515 + "name": "reviewed_at", 2516 + "type": "timestamp with time zone", 2517 + "primaryKey": false, 2518 + "notNull": false 2519 + } 2520 + }, 2521 + "indexes": { 2522 + "mod_queue_author_did_idx": { 2523 + "name": "mod_queue_author_did_idx", 2524 + "columns": [ 2525 + { 2526 + "expression": "author_did", 2527 + "isExpression": false, 2528 + "asc": true, 2529 + "nulls": "last" 2530 + } 2531 + ], 2532 + "isUnique": false, 2533 + "concurrently": false, 2534 + "method": "btree", 2535 + "with": {} 2536 + }, 2537 + "mod_queue_community_did_idx": { 2538 + "name": "mod_queue_community_did_idx", 2539 + "columns": [ 2540 + { 2541 + "expression": "community_did", 2542 + "isExpression": false, 2543 + "asc": true, 2544 + "nulls": "last" 2545 + } 2546 + ], 2547 + "isUnique": false, 2548 + "concurrently": false, 2549 + "method": "btree", 2550 + "with": {} 2551 + }, 2552 + "mod_queue_status_idx": { 2553 + "name": "mod_queue_status_idx", 2554 + "columns": [ 2555 + { 2556 + "expression": "status", 2557 + "isExpression": false, 2558 + "asc": true, 2559 + "nulls": "last" 2560 + } 2561 + ], 2562 + "isUnique": false, 2563 + "concurrently": false, 2564 + "method": "btree", 2565 + "with": {} 2566 + }, 2567 + "mod_queue_created_at_idx": { 2568 + "name": "mod_queue_created_at_idx", 2569 + "columns": [ 2570 + { 2571 + "expression": "created_at", 2572 + "isExpression": false, 2573 + "asc": true, 2574 + "nulls": "last" 2575 + } 2576 + ], 2577 + "isUnique": false, 2578 + "concurrently": false, 2579 + "method": "btree", 2580 + "with": {} 2581 + }, 2582 + "mod_queue_content_uri_idx": { 2583 + "name": "mod_queue_content_uri_idx", 2584 + "columns": [ 2585 + { 2586 + "expression": "content_uri", 2587 + "isExpression": false, 2588 + "asc": true, 2589 + "nulls": "last" 2590 + } 2591 + ], 2592 + "isUnique": false, 2593 + "concurrently": false, 2594 + "method": "btree", 2595 + "with": {} 2596 + } 2597 + }, 2598 + "foreignKeys": {}, 2599 + "compositePrimaryKeys": {}, 2600 + "uniqueConstraints": {}, 2601 + "policies": { 2602 + "tenant_isolation": { 2603 + "name": "tenant_isolation", 2604 + "as": "PERMISSIVE", 2605 + "for": "ALL", 2606 + "to": [ 2607 + "barazo_app" 2608 + ], 2609 + "using": "community_did = current_setting('app.current_community_did', true)", 2610 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2611 + } 2612 + }, 2613 + "checkConstraints": {}, 2614 + "isRLSEnabled": true 2615 + }, 2616 + "public.account_trust": { 2617 + "name": "account_trust", 2618 + "schema": "", 2619 + "columns": { 2620 + "id": { 2621 + "name": "id", 2622 + "type": "serial", 2623 + "primaryKey": true, 2624 + "notNull": true 2625 + }, 2626 + "did": { 2627 + "name": "did", 2628 + "type": "text", 2629 + "primaryKey": false, 2630 + "notNull": true 2631 + }, 2632 + "community_did": { 2633 + "name": "community_did", 2634 + "type": "text", 2635 + "primaryKey": false, 2636 + "notNull": true 2637 + }, 2638 + "approved_post_count": { 2639 + "name": "approved_post_count", 2640 + "type": "integer", 2641 + "primaryKey": false, 2642 + "notNull": true, 2643 + "default": 0 2644 + }, 2645 + "is_trusted": { 2646 + "name": "is_trusted", 2647 + "type": "boolean", 2648 + "primaryKey": false, 2649 + "notNull": true, 2650 + "default": false 2651 + }, 2652 + "trusted_at": { 2653 + "name": "trusted_at", 2654 + "type": "timestamp with time zone", 2655 + "primaryKey": false, 2656 + "notNull": false 2657 + } 2658 + }, 2659 + "indexes": { 2660 + "account_trust_did_community_idx": { 2661 + "name": "account_trust_did_community_idx", 2662 + "columns": [ 2663 + { 2664 + "expression": "did", 2665 + "isExpression": false, 2666 + "asc": true, 2667 + "nulls": "last" 2668 + }, 2669 + { 2670 + "expression": "community_did", 2671 + "isExpression": false, 2672 + "asc": true, 2673 + "nulls": "last" 2674 + } 2675 + ], 2676 + "isUnique": true, 2677 + "concurrently": false, 2678 + "method": "btree", 2679 + "with": {} 2680 + }, 2681 + "account_trust_did_idx": { 2682 + "name": "account_trust_did_idx", 2683 + "columns": [ 2684 + { 2685 + "expression": "did", 2686 + "isExpression": false, 2687 + "asc": true, 2688 + "nulls": "last" 2689 + } 2690 + ], 2691 + "isUnique": false, 2692 + "concurrently": false, 2693 + "method": "btree", 2694 + "with": {} 2695 + } 2696 + }, 2697 + "foreignKeys": {}, 2698 + "compositePrimaryKeys": {}, 2699 + "uniqueConstraints": {}, 2700 + "policies": { 2701 + "tenant_isolation": { 2702 + "name": "tenant_isolation", 2703 + "as": "PERMISSIVE", 2704 + "for": "ALL", 2705 + "to": [ 2706 + "barazo_app" 2707 + ], 2708 + "using": "community_did = current_setting('app.current_community_did', true)", 2709 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2710 + } 2711 + }, 2712 + "checkConstraints": {}, 2713 + "isRLSEnabled": true 2714 + }, 2715 + "public.community_filters": { 2716 + "name": "community_filters", 2717 + "schema": "", 2718 + "columns": { 2719 + "community_did": { 2720 + "name": "community_did", 2721 + "type": "text", 2722 + "primaryKey": true, 2723 + "notNull": true 2724 + }, 2725 + "status": { 2726 + "name": "status", 2727 + "type": "text", 2728 + "primaryKey": false, 2729 + "notNull": true, 2730 + "default": "'active'" 2731 + }, 2732 + "admin_did": { 2733 + "name": "admin_did", 2734 + "type": "text", 2735 + "primaryKey": false, 2736 + "notNull": false 2737 + }, 2738 + "reason": { 2739 + "name": "reason", 2740 + "type": "text", 2741 + "primaryKey": false, 2742 + "notNull": false 2743 + }, 2744 + "report_count": { 2745 + "name": "report_count", 2746 + "type": "integer", 2747 + "primaryKey": false, 2748 + "notNull": true, 2749 + "default": 0 2750 + }, 2751 + "last_reviewed_at": { 2752 + "name": "last_reviewed_at", 2753 + "type": "timestamp with time zone", 2754 + "primaryKey": false, 2755 + "notNull": false 2756 + }, 2757 + "filtered_by": { 2758 + "name": "filtered_by", 2759 + "type": "text", 2760 + "primaryKey": false, 2761 + "notNull": false 2762 + }, 2763 + "created_at": { 2764 + "name": "created_at", 2765 + "type": "timestamp with time zone", 2766 + "primaryKey": false, 2767 + "notNull": true, 2768 + "default": "now()" 2769 + }, 2770 + "updated_at": { 2771 + "name": "updated_at", 2772 + "type": "timestamp with time zone", 2773 + "primaryKey": false, 2774 + "notNull": true, 2775 + "default": "now()" 2776 + } 2777 + }, 2778 + "indexes": { 2779 + "community_filters_status_idx": { 2780 + "name": "community_filters_status_idx", 2781 + "columns": [ 2782 + { 2783 + "expression": "status", 2784 + "isExpression": false, 2785 + "asc": true, 2786 + "nulls": "last" 2787 + } 2788 + ], 2789 + "isUnique": false, 2790 + "concurrently": false, 2791 + "method": "btree", 2792 + "with": {} 2793 + }, 2794 + "community_filters_admin_did_idx": { 2795 + "name": "community_filters_admin_did_idx", 2796 + "columns": [ 2797 + { 2798 + "expression": "admin_did", 2799 + "isExpression": false, 2800 + "asc": true, 2801 + "nulls": "last" 2802 + } 2803 + ], 2804 + "isUnique": false, 2805 + "concurrently": false, 2806 + "method": "btree", 2807 + "with": {} 2808 + }, 2809 + "community_filters_updated_at_idx": { 2810 + "name": "community_filters_updated_at_idx", 2811 + "columns": [ 2812 + { 2813 + "expression": "updated_at", 2814 + "isExpression": false, 2815 + "asc": true, 2816 + "nulls": "last" 2817 + } 2818 + ], 2819 + "isUnique": false, 2820 + "concurrently": false, 2821 + "method": "btree", 2822 + "with": {} 2823 + } 2824 + }, 2825 + "foreignKeys": {}, 2826 + "compositePrimaryKeys": {}, 2827 + "uniqueConstraints": {}, 2828 + "policies": { 2829 + "tenant_isolation": { 2830 + "name": "tenant_isolation", 2831 + "as": "PERMISSIVE", 2832 + "for": "ALL", 2833 + "to": [ 2834 + "barazo_app" 2835 + ], 2836 + "using": "community_did = current_setting('app.current_community_did', true)", 2837 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2838 + } 2839 + }, 2840 + "checkConstraints": {}, 2841 + "isRLSEnabled": true 2842 + }, 2843 + "public.account_filters": { 2844 + "name": "account_filters", 2845 + "schema": "", 2846 + "columns": { 2847 + "id": { 2848 + "name": "id", 2849 + "type": "serial", 2850 + "primaryKey": true, 2851 + "notNull": true 2852 + }, 2853 + "did": { 2854 + "name": "did", 2855 + "type": "text", 2856 + "primaryKey": false, 2857 + "notNull": true 2858 + }, 2859 + "community_did": { 2860 + "name": "community_did", 2861 + "type": "text", 2862 + "primaryKey": false, 2863 + "notNull": true 2864 + }, 2865 + "status": { 2866 + "name": "status", 2867 + "type": "text", 2868 + "primaryKey": false, 2869 + "notNull": true, 2870 + "default": "'active'" 2871 + }, 2872 + "reason": { 2873 + "name": "reason", 2874 + "type": "text", 2875 + "primaryKey": false, 2876 + "notNull": false 2877 + }, 2878 + "report_count": { 2879 + "name": "report_count", 2880 + "type": "integer", 2881 + "primaryKey": false, 2882 + "notNull": true, 2883 + "default": 0 2884 + }, 2885 + "ban_count": { 2886 + "name": "ban_count", 2887 + "type": "integer", 2888 + "primaryKey": false, 2889 + "notNull": true, 2890 + "default": 0 2891 + }, 2892 + "last_reviewed_at": { 2893 + "name": "last_reviewed_at", 2894 + "type": "timestamp with time zone", 2895 + "primaryKey": false, 2896 + "notNull": false 2897 + }, 2898 + "filtered_by": { 2899 + "name": "filtered_by", 2900 + "type": "text", 2901 + "primaryKey": false, 2902 + "notNull": false 2903 + }, 2904 + "created_at": { 2905 + "name": "created_at", 2906 + "type": "timestamp with time zone", 2907 + "primaryKey": false, 2908 + "notNull": true, 2909 + "default": "now()" 2910 + }, 2911 + "updated_at": { 2912 + "name": "updated_at", 2913 + "type": "timestamp with time zone", 2914 + "primaryKey": false, 2915 + "notNull": true, 2916 + "default": "now()" 2917 + } 2918 + }, 2919 + "indexes": { 2920 + "account_filters_did_community_idx": { 2921 + "name": "account_filters_did_community_idx", 2922 + "columns": [ 2923 + { 2924 + "expression": "did", 2925 + "isExpression": false, 2926 + "asc": true, 2927 + "nulls": "last" 2928 + }, 2929 + { 2930 + "expression": "community_did", 2931 + "isExpression": false, 2932 + "asc": true, 2933 + "nulls": "last" 2934 + } 2935 + ], 2936 + "isUnique": true, 2937 + "concurrently": false, 2938 + "method": "btree", 2939 + "with": {} 2940 + }, 2941 + "account_filters_did_idx": { 2942 + "name": "account_filters_did_idx", 2943 + "columns": [ 2944 + { 2945 + "expression": "did", 2946 + "isExpression": false, 2947 + "asc": true, 2948 + "nulls": "last" 2949 + } 2950 + ], 2951 + "isUnique": false, 2952 + "concurrently": false, 2953 + "method": "btree", 2954 + "with": {} 2955 + }, 2956 + "account_filters_community_did_idx": { 2957 + "name": "account_filters_community_did_idx", 2958 + "columns": [ 2959 + { 2960 + "expression": "community_did", 2961 + "isExpression": false, 2962 + "asc": true, 2963 + "nulls": "last" 2964 + } 2965 + ], 2966 + "isUnique": false, 2967 + "concurrently": false, 2968 + "method": "btree", 2969 + "with": {} 2970 + }, 2971 + "account_filters_status_idx": { 2972 + "name": "account_filters_status_idx", 2973 + "columns": [ 2974 + { 2975 + "expression": "status", 2976 + "isExpression": false, 2977 + "asc": true, 2978 + "nulls": "last" 2979 + } 2980 + ], 2981 + "isUnique": false, 2982 + "concurrently": false, 2983 + "method": "btree", 2984 + "with": {} 2985 + }, 2986 + "account_filters_updated_at_idx": { 2987 + "name": "account_filters_updated_at_idx", 2988 + "columns": [ 2989 + { 2990 + "expression": "updated_at", 2991 + "isExpression": false, 2992 + "asc": true, 2993 + "nulls": "last" 2994 + } 2995 + ], 2996 + "isUnique": false, 2997 + "concurrently": false, 2998 + "method": "btree", 2999 + "with": {} 3000 + } 3001 + }, 3002 + "foreignKeys": {}, 3003 + "compositePrimaryKeys": {}, 3004 + "uniqueConstraints": {}, 3005 + "policies": { 3006 + "tenant_isolation": { 3007 + "name": "tenant_isolation", 3008 + "as": "PERMISSIVE", 3009 + "for": "ALL", 3010 + "to": [ 3011 + "barazo_app" 3012 + ], 3013 + "using": "community_did = current_setting('app.current_community_did', true)", 3014 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 3015 + } 3016 + }, 3017 + "checkConstraints": {}, 3018 + "isRLSEnabled": true 3019 + }, 3020 + "public.ozone_labels": { 3021 + "name": "ozone_labels", 3022 + "schema": "", 3023 + "columns": { 3024 + "id": { 3025 + "name": "id", 3026 + "type": "serial", 3027 + "primaryKey": true, 3028 + "notNull": true 3029 + }, 3030 + "src": { 3031 + "name": "src", 3032 + "type": "text", 3033 + "primaryKey": false, 3034 + "notNull": true 3035 + }, 3036 + "uri": { 3037 + "name": "uri", 3038 + "type": "text", 3039 + "primaryKey": false, 3040 + "notNull": true 3041 + }, 3042 + "val": { 3043 + "name": "val", 3044 + "type": "text", 3045 + "primaryKey": false, 3046 + "notNull": true 3047 + }, 3048 + "neg": { 3049 + "name": "neg", 3050 + "type": "boolean", 3051 + "primaryKey": false, 3052 + "notNull": true, 3053 + "default": false 3054 + }, 3055 + "cts": { 3056 + "name": "cts", 3057 + "type": "timestamp with time zone", 3058 + "primaryKey": false, 3059 + "notNull": true 3060 + }, 3061 + "exp": { 3062 + "name": "exp", 3063 + "type": "timestamp with time zone", 3064 + "primaryKey": false, 3065 + "notNull": false 3066 + }, 3067 + "indexed_at": { 3068 + "name": "indexed_at", 3069 + "type": "timestamp with time zone", 3070 + "primaryKey": false, 3071 + "notNull": true, 3072 + "default": "now()" 3073 + } 3074 + }, 3075 + "indexes": { 3076 + "ozone_labels_src_uri_val_idx": { 3077 + "name": "ozone_labels_src_uri_val_idx", 3078 + "columns": [ 3079 + { 3080 + "expression": "src", 3081 + "isExpression": false, 3082 + "asc": true, 3083 + "nulls": "last" 3084 + }, 3085 + { 3086 + "expression": "uri", 3087 + "isExpression": false, 3088 + "asc": true, 3089 + "nulls": "last" 3090 + }, 3091 + { 3092 + "expression": "val", 3093 + "isExpression": false, 3094 + "asc": true, 3095 + "nulls": "last" 3096 + } 3097 + ], 3098 + "isUnique": true, 3099 + "concurrently": false, 3100 + "method": "btree", 3101 + "with": {} 3102 + }, 3103 + "ozone_labels_uri_idx": { 3104 + "name": "ozone_labels_uri_idx", 3105 + "columns": [ 3106 + { 3107 + "expression": "uri", 3108 + "isExpression": false, 3109 + "asc": true, 3110 + "nulls": "last" 3111 + } 3112 + ], 3113 + "isUnique": false, 3114 + "concurrently": false, 3115 + "method": "btree", 3116 + "with": {} 3117 + }, 3118 + "ozone_labels_val_idx": { 3119 + "name": "ozone_labels_val_idx", 3120 + "columns": [ 3121 + { 3122 + "expression": "val", 3123 + "isExpression": false, 3124 + "asc": true, 3125 + "nulls": "last" 3126 + } 3127 + ], 3128 + "isUnique": false, 3129 + "concurrently": false, 3130 + "method": "btree", 3131 + "with": {} 3132 + }, 3133 + "ozone_labels_indexed_at_idx": { 3134 + "name": "ozone_labels_indexed_at_idx", 3135 + "columns": [ 3136 + { 3137 + "expression": "indexed_at", 3138 + "isExpression": false, 3139 + "asc": true, 3140 + "nulls": "last" 3141 + } 3142 + ], 3143 + "isUnique": false, 3144 + "concurrently": false, 3145 + "method": "btree", 3146 + "with": {} 3147 + } 3148 + }, 3149 + "foreignKeys": {}, 3150 + "compositePrimaryKeys": {}, 3151 + "uniqueConstraints": {}, 3152 + "policies": {}, 3153 + "checkConstraints": {}, 3154 + "isRLSEnabled": false 3155 + }, 3156 + "public.community_profiles": { 3157 + "name": "community_profiles", 3158 + "schema": "", 3159 + "columns": { 3160 + "did": { 3161 + "name": "did", 3162 + "type": "text", 3163 + "primaryKey": false, 3164 + "notNull": true 3165 + }, 3166 + "community_did": { 3167 + "name": "community_did", 3168 + "type": "text", 3169 + "primaryKey": false, 3170 + "notNull": true 3171 + }, 3172 + "display_name": { 3173 + "name": "display_name", 3174 + "type": "text", 3175 + "primaryKey": false, 3176 + "notNull": false 3177 + }, 3178 + "avatar_url": { 3179 + "name": "avatar_url", 3180 + "type": "text", 3181 + "primaryKey": false, 3182 + "notNull": false 3183 + }, 3184 + "banner_url": { 3185 + "name": "banner_url", 3186 + "type": "text", 3187 + "primaryKey": false, 3188 + "notNull": false 3189 + }, 3190 + "bio": { 3191 + "name": "bio", 3192 + "type": "text", 3193 + "primaryKey": false, 3194 + "notNull": false 3195 + }, 3196 + "updated_at": { 3197 + "name": "updated_at", 3198 + "type": "timestamp with time zone", 3199 + "primaryKey": false, 3200 + "notNull": true, 3201 + "default": "now()" 3202 + } 3203 + }, 3204 + "indexes": { 3205 + "community_profiles_did_idx": { 3206 + "name": "community_profiles_did_idx", 3207 + "columns": [ 3208 + { 3209 + "expression": "did", 3210 + "isExpression": false, 3211 + "asc": true, 3212 + "nulls": "last" 3213 + } 3214 + ], 3215 + "isUnique": false, 3216 + "concurrently": false, 3217 + "method": "btree", 3218 + "with": {} 3219 + }, 3220 + "community_profiles_community_idx": { 3221 + "name": "community_profiles_community_idx", 3222 + "columns": [ 3223 + { 3224 + "expression": "community_did", 3225 + "isExpression": false, 3226 + "asc": true, 3227 + "nulls": "last" 3228 + } 3229 + ], 3230 + "isUnique": false, 3231 + "concurrently": false, 3232 + "method": "btree", 3233 + "with": {} 3234 + } 3235 + }, 3236 + "foreignKeys": {}, 3237 + "compositePrimaryKeys": { 3238 + "community_profiles_did_community_did_pk": { 3239 + "name": "community_profiles_did_community_did_pk", 3240 + "columns": [ 3241 + "did", 3242 + "community_did" 3243 + ] 3244 + } 3245 + }, 3246 + "uniqueConstraints": {}, 3247 + "policies": { 3248 + "tenant_isolation": { 3249 + "name": "tenant_isolation", 3250 + "as": "PERMISSIVE", 3251 + "for": "ALL", 3252 + "to": [ 3253 + "barazo_app" 3254 + ], 3255 + "using": "community_did = current_setting('app.current_community_did', true)", 3256 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 3257 + } 3258 + }, 3259 + "checkConstraints": {}, 3260 + "isRLSEnabled": true 3261 + }, 3262 + "public.interaction_graph": { 3263 + "name": "interaction_graph", 3264 + "schema": "", 3265 + "columns": { 3266 + "source_did": { 3267 + "name": "source_did", 3268 + "type": "text", 3269 + "primaryKey": false, 3270 + "notNull": true 3271 + }, 3272 + "target_did": { 3273 + "name": "target_did", 3274 + "type": "text", 3275 + "primaryKey": false, 3276 + "notNull": true 3277 + }, 3278 + "community_id": { 3279 + "name": "community_id", 3280 + "type": "text", 3281 + "primaryKey": false, 3282 + "notNull": true 3283 + }, 3284 + "interaction_type": { 3285 + "name": "interaction_type", 3286 + "type": "text", 3287 + "primaryKey": false, 3288 + "notNull": true 3289 + }, 3290 + "weight": { 3291 + "name": "weight", 3292 + "type": "integer", 3293 + "primaryKey": false, 3294 + "notNull": true, 3295 + "default": 1 3296 + }, 3297 + "first_interaction_at": { 3298 + "name": "first_interaction_at", 3299 + "type": "timestamp with time zone", 3300 + "primaryKey": false, 3301 + "notNull": true, 3302 + "default": "now()" 3303 + }, 3304 + "last_interaction_at": { 3305 + "name": "last_interaction_at", 3306 + "type": "timestamp with time zone", 3307 + "primaryKey": false, 3308 + "notNull": true, 3309 + "default": "now()" 3310 + } 3311 + }, 3312 + "indexes": { 3313 + "interaction_graph_source_target_community_idx": { 3314 + "name": "interaction_graph_source_target_community_idx", 3315 + "columns": [ 3316 + { 3317 + "expression": "source_did", 3318 + "isExpression": false, 3319 + "asc": true, 3320 + "nulls": "last" 3321 + }, 3322 + { 3323 + "expression": "target_did", 3324 + "isExpression": false, 3325 + "asc": true, 3326 + "nulls": "last" 3327 + }, 3328 + { 3329 + "expression": "community_id", 3330 + "isExpression": false, 3331 + "asc": true, 3332 + "nulls": "last" 3333 + } 3334 + ], 3335 + "isUnique": false, 3336 + "concurrently": false, 3337 + "method": "btree", 3338 + "with": {} 3339 + } 3340 + }, 3341 + "foreignKeys": {}, 3342 + "compositePrimaryKeys": { 3343 + "interaction_graph_source_did_target_did_community_id_interaction_type_pk": { 3344 + "name": "interaction_graph_source_did_target_did_community_id_interaction_type_pk", 3345 + "columns": [ 3346 + "source_did", 3347 + "target_did", 3348 + "community_id", 3349 + "interaction_type" 3350 + ] 3351 + } 3352 + }, 3353 + "uniqueConstraints": {}, 3354 + "policies": {}, 3355 + "checkConstraints": {}, 3356 + "isRLSEnabled": false 3357 + }, 3358 + "public.trust_seeds": { 3359 + "name": "trust_seeds", 3360 + "schema": "", 3361 + "columns": { 3362 + "id": { 3363 + "name": "id", 3364 + "type": "serial", 3365 + "primaryKey": true, 3366 + "notNull": true 3367 + }, 3368 + "did": { 3369 + "name": "did", 3370 + "type": "text", 3371 + "primaryKey": false, 3372 + "notNull": true 3373 + }, 3374 + "community_id": { 3375 + "name": "community_id", 3376 + "type": "text", 3377 + "primaryKey": false, 3378 + "notNull": true, 3379 + "default": "''" 3380 + }, 3381 + "added_by": { 3382 + "name": "added_by", 3383 + "type": "text", 3384 + "primaryKey": false, 3385 + "notNull": true 3386 + }, 3387 + "reason": { 3388 + "name": "reason", 3389 + "type": "text", 3390 + "primaryKey": false, 3391 + "notNull": false 3392 + }, 3393 + "created_at": { 3394 + "name": "created_at", 3395 + "type": "timestamp with time zone", 3396 + "primaryKey": false, 3397 + "notNull": true, 3398 + "default": "now()" 3399 + } 3400 + }, 3401 + "indexes": { 3402 + "trust_seeds_did_community_idx": { 3403 + "name": "trust_seeds_did_community_idx", 3404 + "columns": [ 3405 + { 3406 + "expression": "did", 3407 + "isExpression": false, 3408 + "asc": true, 3409 + "nulls": "last" 3410 + }, 3411 + { 3412 + "expression": "community_id", 3413 + "isExpression": false, 3414 + "asc": true, 3415 + "nulls": "last" 3416 + } 3417 + ], 3418 + "isUnique": true, 3419 + "concurrently": false, 3420 + "method": "btree", 3421 + "with": {} 3422 + } 3423 + }, 3424 + "foreignKeys": {}, 3425 + "compositePrimaryKeys": {}, 3426 + "uniqueConstraints": {}, 3427 + "policies": {}, 3428 + "checkConstraints": {}, 3429 + "isRLSEnabled": false 3430 + }, 3431 + "public.trust_scores": { 3432 + "name": "trust_scores", 3433 + "schema": "", 3434 + "columns": { 3435 + "did": { 3436 + "name": "did", 3437 + "type": "text", 3438 + "primaryKey": false, 3439 + "notNull": true 3440 + }, 3441 + "community_id": { 3442 + "name": "community_id", 3443 + "type": "text", 3444 + "primaryKey": false, 3445 + "notNull": true, 3446 + "default": "''" 3447 + }, 3448 + "score": { 3449 + "name": "score", 3450 + "type": "real", 3451 + "primaryKey": false, 3452 + "notNull": true 3453 + }, 3454 + "computed_at": { 3455 + "name": "computed_at", 3456 + "type": "timestamp with time zone", 3457 + "primaryKey": false, 3458 + "notNull": true, 3459 + "default": "now()" 3460 + } 3461 + }, 3462 + "indexes": { 3463 + "trust_scores_did_community_idx": { 3464 + "name": "trust_scores_did_community_idx", 3465 + "columns": [ 3466 + { 3467 + "expression": "did", 3468 + "isExpression": false, 3469 + "asc": true, 3470 + "nulls": "last" 3471 + }, 3472 + { 3473 + "expression": "community_id", 3474 + "isExpression": false, 3475 + "asc": true, 3476 + "nulls": "last" 3477 + } 3478 + ], 3479 + "isUnique": false, 3480 + "concurrently": false, 3481 + "method": "btree", 3482 + "with": {} 3483 + } 3484 + }, 3485 + "foreignKeys": {}, 3486 + "compositePrimaryKeys": { 3487 + "trust_scores_did_community_id_pk": { 3488 + "name": "trust_scores_did_community_id_pk", 3489 + "columns": [ 3490 + "did", 3491 + "community_id" 3492 + ] 3493 + } 3494 + }, 3495 + "uniqueConstraints": {}, 3496 + "policies": {}, 3497 + "checkConstraints": {}, 3498 + "isRLSEnabled": false 3499 + }, 3500 + "public.sybil_clusters": { 3501 + "name": "sybil_clusters", 3502 + "schema": "", 3503 + "columns": { 3504 + "id": { 3505 + "name": "id", 3506 + "type": "serial", 3507 + "primaryKey": true, 3508 + "notNull": true 3509 + }, 3510 + "cluster_hash": { 3511 + "name": "cluster_hash", 3512 + "type": "text", 3513 + "primaryKey": false, 3514 + "notNull": true 3515 + }, 3516 + "internal_edge_count": { 3517 + "name": "internal_edge_count", 3518 + "type": "integer", 3519 + "primaryKey": false, 3520 + "notNull": true 3521 + }, 3522 + "external_edge_count": { 3523 + "name": "external_edge_count", 3524 + "type": "integer", 3525 + "primaryKey": false, 3526 + "notNull": true 3527 + }, 3528 + "member_count": { 3529 + "name": "member_count", 3530 + "type": "integer", 3531 + "primaryKey": false, 3532 + "notNull": true 3533 + }, 3534 + "status": { 3535 + "name": "status", 3536 + "type": "text", 3537 + "primaryKey": false, 3538 + "notNull": true, 3539 + "default": "'flagged'" 3540 + }, 3541 + "reviewed_by": { 3542 + "name": "reviewed_by", 3543 + "type": "text", 3544 + "primaryKey": false, 3545 + "notNull": false 3546 + }, 3547 + "reviewed_at": { 3548 + "name": "reviewed_at", 3549 + "type": "timestamp with time zone", 3550 + "primaryKey": false, 3551 + "notNull": false 3552 + }, 3553 + "detected_at": { 3554 + "name": "detected_at", 3555 + "type": "timestamp with time zone", 3556 + "primaryKey": false, 3557 + "notNull": true, 3558 + "default": "now()" 3559 + }, 3560 + "updated_at": { 3561 + "name": "updated_at", 3562 + "type": "timestamp with time zone", 3563 + "primaryKey": false, 3564 + "notNull": true, 3565 + "default": "now()" 3566 + } 3567 + }, 3568 + "indexes": { 3569 + "sybil_clusters_hash_idx": { 3570 + "name": "sybil_clusters_hash_idx", 3571 + "columns": [ 3572 + { 3573 + "expression": "cluster_hash", 3574 + "isExpression": false, 3575 + "asc": true, 3576 + "nulls": "last" 3577 + } 3578 + ], 3579 + "isUnique": true, 3580 + "concurrently": false, 3581 + "method": "btree", 3582 + "with": {} 3583 + } 3584 + }, 3585 + "foreignKeys": {}, 3586 + "compositePrimaryKeys": {}, 3587 + "uniqueConstraints": {}, 3588 + "policies": {}, 3589 + "checkConstraints": {}, 3590 + "isRLSEnabled": false 3591 + }, 3592 + "public.sybil_cluster_members": { 3593 + "name": "sybil_cluster_members", 3594 + "schema": "", 3595 + "columns": { 3596 + "cluster_id": { 3597 + "name": "cluster_id", 3598 + "type": "integer", 3599 + "primaryKey": false, 3600 + "notNull": true 3601 + }, 3602 + "did": { 3603 + "name": "did", 3604 + "type": "text", 3605 + "primaryKey": false, 3606 + "notNull": true 3607 + }, 3608 + "role_in_cluster": { 3609 + "name": "role_in_cluster", 3610 + "type": "text", 3611 + "primaryKey": false, 3612 + "notNull": true 3613 + }, 3614 + "joined_at": { 3615 + "name": "joined_at", 3616 + "type": "timestamp with time zone", 3617 + "primaryKey": false, 3618 + "notNull": true, 3619 + "default": "now()" 3620 + } 3621 + }, 3622 + "indexes": {}, 3623 + "foreignKeys": { 3624 + "sybil_cluster_members_cluster_id_sybil_clusters_id_fk": { 3625 + "name": "sybil_cluster_members_cluster_id_sybil_clusters_id_fk", 3626 + "tableFrom": "sybil_cluster_members", 3627 + "tableTo": "sybil_clusters", 3628 + "columnsFrom": [ 3629 + "cluster_id" 3630 + ], 3631 + "columnsTo": [ 3632 + "id" 3633 + ], 3634 + "onDelete": "no action", 3635 + "onUpdate": "no action" 3636 + } 3637 + }, 3638 + "compositePrimaryKeys": { 3639 + "sybil_cluster_members_cluster_id_did_pk": { 3640 + "name": "sybil_cluster_members_cluster_id_did_pk", 3641 + "columns": [ 3642 + "cluster_id", 3643 + "did" 3644 + ] 3645 + } 3646 + }, 3647 + "uniqueConstraints": {}, 3648 + "policies": {}, 3649 + "checkConstraints": {}, 3650 + "isRLSEnabled": false 3651 + }, 3652 + "public.behavioral_flags": { 3653 + "name": "behavioral_flags", 3654 + "schema": "", 3655 + "columns": { 3656 + "id": { 3657 + "name": "id", 3658 + "type": "serial", 3659 + "primaryKey": true, 3660 + "notNull": true 3661 + }, 3662 + "flag_type": { 3663 + "name": "flag_type", 3664 + "type": "text", 3665 + "primaryKey": false, 3666 + "notNull": true 3667 + }, 3668 + "affected_dids": { 3669 + "name": "affected_dids", 3670 + "type": "jsonb", 3671 + "primaryKey": false, 3672 + "notNull": true 3673 + }, 3674 + "details": { 3675 + "name": "details", 3676 + "type": "text", 3677 + "primaryKey": false, 3678 + "notNull": true 3679 + }, 3680 + "community_did": { 3681 + "name": "community_did", 3682 + "type": "text", 3683 + "primaryKey": false, 3684 + "notNull": false 3685 + }, 3686 + "status": { 3687 + "name": "status", 3688 + "type": "text", 3689 + "primaryKey": false, 3690 + "notNull": true, 3691 + "default": "'pending'" 3692 + }, 3693 + "detected_at": { 3694 + "name": "detected_at", 3695 + "type": "timestamp with time zone", 3696 + "primaryKey": false, 3697 + "notNull": true, 3698 + "default": "now()" 3699 + } 3700 + }, 3701 + "indexes": { 3702 + "behavioral_flags_flag_type_idx": { 3703 + "name": "behavioral_flags_flag_type_idx", 3704 + "columns": [ 3705 + { 3706 + "expression": "flag_type", 3707 + "isExpression": false, 3708 + "asc": true, 3709 + "nulls": "last" 3710 + } 3711 + ], 3712 + "isUnique": false, 3713 + "concurrently": false, 3714 + "method": "btree", 3715 + "with": {} 3716 + }, 3717 + "behavioral_flags_status_idx": { 3718 + "name": "behavioral_flags_status_idx", 3719 + "columns": [ 3720 + { 3721 + "expression": "status", 3722 + "isExpression": false, 3723 + "asc": true, 3724 + "nulls": "last" 3725 + } 3726 + ], 3727 + "isUnique": false, 3728 + "concurrently": false, 3729 + "method": "btree", 3730 + "with": {} 3731 + }, 3732 + "behavioral_flags_detected_at_idx": { 3733 + "name": "behavioral_flags_detected_at_idx", 3734 + "columns": [ 3735 + { 3736 + "expression": "detected_at", 3737 + "isExpression": false, 3738 + "asc": true, 3739 + "nulls": "last" 3740 + } 3741 + ], 3742 + "isUnique": false, 3743 + "concurrently": false, 3744 + "method": "btree", 3745 + "with": {} 3746 + } 3747 + }, 3748 + "foreignKeys": {}, 3749 + "compositePrimaryKeys": {}, 3750 + "uniqueConstraints": {}, 3751 + "policies": {}, 3752 + "checkConstraints": {}, 3753 + "isRLSEnabled": false 3754 + }, 3755 + "public.pds_trust_factors": { 3756 + "name": "pds_trust_factors", 3757 + "schema": "", 3758 + "columns": { 3759 + "id": { 3760 + "name": "id", 3761 + "type": "serial", 3762 + "primaryKey": true, 3763 + "notNull": true 3764 + }, 3765 + "pds_host": { 3766 + "name": "pds_host", 3767 + "type": "text", 3768 + "primaryKey": false, 3769 + "notNull": true 3770 + }, 3771 + "trust_factor": { 3772 + "name": "trust_factor", 3773 + "type": "real", 3774 + "primaryKey": false, 3775 + "notNull": true 3776 + }, 3777 + "is_default": { 3778 + "name": "is_default", 3779 + "type": "boolean", 3780 + "primaryKey": false, 3781 + "notNull": true, 3782 + "default": false 3783 + }, 3784 + "updated_at": { 3785 + "name": "updated_at", 3786 + "type": "timestamp with time zone", 3787 + "primaryKey": false, 3788 + "notNull": true, 3789 + "default": "now()" 3790 + } 3791 + }, 3792 + "indexes": { 3793 + "pds_trust_factors_pds_host_idx": { 3794 + "name": "pds_trust_factors_pds_host_idx", 3795 + "columns": [ 3796 + { 3797 + "expression": "pds_host", 3798 + "isExpression": false, 3799 + "asc": true, 3800 + "nulls": "last" 3801 + } 3802 + ], 3803 + "isUnique": true, 3804 + "concurrently": false, 3805 + "method": "btree", 3806 + "with": {} 3807 + } 3808 + }, 3809 + "foreignKeys": {}, 3810 + "compositePrimaryKeys": {}, 3811 + "uniqueConstraints": {}, 3812 + "policies": {}, 3813 + "checkConstraints": {}, 3814 + "isRLSEnabled": false 3815 + } 3816 + }, 3817 + "enums": {}, 3818 + "schemas": {}, 3819 + "sequences": {}, 3820 + "roles": { 3821 + "barazo_app": { 3822 + "name": "barazo_app", 3823 + "createDb": false, 3824 + "createRole": false, 3825 + "inherit": true 3826 + } 3827 + }, 3828 + "policies": {}, 3829 + "views": {}, 3830 + "_meta": { 3831 + "columns": {}, 3832 + "schemas": {}, 3833 + "tables": {} 3834 + } 3835 + }
+8 -1
drizzle/meta/_journal.json
··· 8 8 "when": 1772367640029, 9 9 "tag": "0000_baseline", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "7", 15 + "when": 1772555130464, 16 + "tag": "0001_add_favicon_url", 17 + "breakpoints": true 11 18 } 12 19 ] 13 - } 20 + }
+2
src/app.ts
··· 43 43 import { communityProfileRoutes } from './routes/community-profiles.js' 44 44 import { uploadRoutes } from './routes/uploads.js' 45 45 import { adminSybilRoutes } from './routes/admin-sybil.js' 46 + import { adminDesignRoutes } from './routes/admin-design.js' 46 47 import { createRequireAdmin } from './auth/require-admin.js' 47 48 import { createRequireOperator } from './auth/require-operator.js' 48 49 import { OzoneService } from './services/ozone.js' ··· 336 337 await app.register(communityProfileRoutes()) 337 338 await app.register(uploadRoutes()) 338 339 await app.register(adminSybilRoutes()) 340 + await app.register(adminDesignRoutes()) 339 341 340 342 // OpenAPI spec endpoint (after routes so all schemas are registered) 341 343 app.get('/api/openapi.json', { schema: { hide: true } }, async (_request, reply) => {
+1
src/db/schema/community-settings.ts
··· 52 52 signingKey: text('signing_key'), 53 53 rotationKey: text('rotation_key'), 54 54 communityLogoUrl: text('community_logo_url'), 55 + faviconUrl: text('favicon_url'), 55 56 primaryColor: text('primary_color'), 56 57 accentColor: text('accent_color'), 57 58 createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+144
src/routes/admin-design.ts
··· 1 + import type { FastifyPluginCallback } from 'fastify' 2 + import { eq } from 'drizzle-orm' 3 + import sharp from 'sharp' 4 + import { badRequest, errorResponseSchema } from '../lib/api-errors.js' 5 + import { requireCommunityDid } from '../middleware/community-resolver.js' 6 + import { communitySettings } from '../db/schema/community-settings.js' 7 + 8 + const ALLOWED_MIMES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']) 9 + 10 + const LOGO_SIZE = { width: 512, height: 512 } 11 + const FAVICON_SIZE = { width: 256, height: 256 } 12 + 13 + // --------------------------------------------------------------------------- 14 + // OpenAPI JSON Schema definitions 15 + // --------------------------------------------------------------------------- 16 + 17 + const uploadResponseJsonSchema = { 18 + type: 'object' as const, 19 + properties: { 20 + url: { type: 'string' as const }, 21 + }, 22 + } 23 + 24 + // --------------------------------------------------------------------------- 25 + // Admin design routes plugin 26 + // --------------------------------------------------------------------------- 27 + 28 + /** 29 + * Logo and favicon upload endpoints for community design. 30 + * 31 + * - POST /api/admin/design/logo -- Upload community logo 32 + * - POST /api/admin/design/favicon -- Upload community favicon 33 + */ 34 + export function adminDesignRoutes(): FastifyPluginCallback { 35 + return (app, _opts, done) => { 36 + const { db, storage, env } = app 37 + const requireAdmin = app.requireAdmin 38 + const maxSize = env.UPLOAD_MAX_SIZE_BYTES 39 + 40 + // ----------------------------------------------------------------- 41 + // POST /api/admin/design/logo 42 + // ----------------------------------------------------------------- 43 + 44 + app.post( 45 + '/api/admin/design/logo', 46 + { 47 + preHandler: [requireAdmin], 48 + schema: { 49 + tags: ['Admin', 'Design'], 50 + summary: 'Upload community logo', 51 + security: [{ bearerAuth: [] }], 52 + consumes: ['multipart/form-data'], 53 + response: { 54 + 200: uploadResponseJsonSchema, 55 + 400: errorResponseSchema, 56 + 401: errorResponseSchema, 57 + 403: errorResponseSchema, 58 + }, 59 + }, 60 + }, 61 + async (request, reply) => { 62 + const communityDid = requireCommunityDid(request) 63 + 64 + const file = await request.file() 65 + if (!file) throw badRequest('No file uploaded') 66 + if (!ALLOWED_MIMES.has(file.mimetype)) { 67 + throw badRequest('File must be JPEG, PNG, WebP, or GIF') 68 + } 69 + 70 + const buffer = await file.toBuffer() 71 + if (buffer.length > maxSize) { 72 + throw badRequest(`File too large (max ${String(Math.round(maxSize / 1024 / 1024))}MB)`) 73 + } 74 + 75 + const processed = await sharp(buffer) 76 + .resize(LOGO_SIZE.width, LOGO_SIZE.height, { fit: 'cover' }) 77 + .webp({ quality: 85 }) 78 + .toBuffer() 79 + 80 + const url = await storage.store(processed, 'image/webp', 'logos') 81 + 82 + await db 83 + .update(communitySettings) 84 + .set({ communityLogoUrl: url, updatedAt: new Date() }) 85 + .where(eq(communitySettings.communityDid, communityDid)) 86 + 87 + return reply.status(200).send({ url }) 88 + } 89 + ) 90 + 91 + // ----------------------------------------------------------------- 92 + // POST /api/admin/design/favicon 93 + // ----------------------------------------------------------------- 94 + 95 + app.post( 96 + '/api/admin/design/favicon', 97 + { 98 + preHandler: [requireAdmin], 99 + schema: { 100 + tags: ['Admin', 'Design'], 101 + summary: 'Upload community favicon', 102 + security: [{ bearerAuth: [] }], 103 + consumes: ['multipart/form-data'], 104 + response: { 105 + 200: uploadResponseJsonSchema, 106 + 400: errorResponseSchema, 107 + 401: errorResponseSchema, 108 + 403: errorResponseSchema, 109 + }, 110 + }, 111 + }, 112 + async (request, reply) => { 113 + const communityDid = requireCommunityDid(request) 114 + 115 + const file = await request.file() 116 + if (!file) throw badRequest('No file uploaded') 117 + if (!ALLOWED_MIMES.has(file.mimetype)) { 118 + throw badRequest('File must be JPEG, PNG, WebP, or GIF') 119 + } 120 + 121 + const buffer = await file.toBuffer() 122 + if (buffer.length > maxSize) { 123 + throw badRequest(`File too large (max ${String(Math.round(maxSize / 1024 / 1024))}MB)`) 124 + } 125 + 126 + const processed = await sharp(buffer) 127 + .resize(FAVICON_SIZE.width, FAVICON_SIZE.height, { fit: 'cover' }) 128 + .webp({ quality: 90 }) 129 + .toBuffer() 130 + 131 + const url = await storage.store(processed, 'image/webp', 'favicons') 132 + 133 + await db 134 + .update(communitySettings) 135 + .set({ faviconUrl: url, updatedAt: new Date() }) 136 + .where(eq(communitySettings.communityDid, communityDid)) 137 + 138 + return reply.status(200).send({ url }) 139 + } 140 + ) 141 + 142 + done() 143 + } 144 + }
+9
src/routes/admin-settings.ts
··· 22 22 reactionSet: { type: 'array' as const, items: { type: 'string' as const } }, 23 23 communityDescription: { type: ['string', 'null'] as const }, 24 24 communityLogoUrl: { type: ['string', 'null'] as const }, 25 + faviconUrl: { type: ['string', 'null'] as const }, 25 26 primaryColor: { type: ['string', 'null'] as const }, 26 27 accentColor: { type: ['string', 'null'] as const }, 27 28 jurisdictionCountry: { type: ['string', 'null'] as const }, ··· 86 87 reactionSet: row.reactionSet, 87 88 communityDescription: row.communityDescription ?? null, 88 89 communityLogoUrl: row.communityLogoUrl ?? null, 90 + faviconUrl: row.faviconUrl ?? null, 89 91 primaryColor: row.primaryColor ?? null, 90 92 accentColor: row.accentColor ?? null, 91 93 jurisdictionCountry: row.jurisdictionCountry ?? null, ··· 131 133 maturityRating: { type: 'string' as const, enum: ['safe', 'mature', 'adult'] }, 132 134 communityDescription: { type: ['string', 'null'] as const }, 133 135 communityLogoUrl: { type: ['string', 'null'] as const }, 136 + faviconUrl: { type: ['string', 'null'] as const }, 134 137 }, 135 138 }, 136 139 404: errorResponseSchema, ··· 155 158 maturityRating: row.maturityRating, 156 159 communityDescription: row.communityDescription ?? null, 157 160 communityLogoUrl: row.communityLogoUrl ?? null, 161 + faviconUrl: row.faviconUrl ?? null, 158 162 }) 159 163 } 160 164 ) ··· 219 223 }, 220 224 communityDescription: { type: ['string', 'null'], maxLength: 500 }, 221 225 communityLogoUrl: { type: ['string', 'null'], format: 'uri' }, 226 + faviconUrl: { type: ['string', 'null'], format: 'uri' }, 222 227 primaryColor: { 223 228 type: ['string', 'null'], 224 229 pattern: '^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$', ··· 258 263 updates.reactionSet === undefined && 259 264 updates.communityDescription === undefined && 260 265 updates.communityLogoUrl === undefined && 266 + updates.faviconUrl === undefined && 261 267 updates.primaryColor === undefined && 262 268 updates.accentColor === undefined && 263 269 updates.jurisdictionCountry === undefined && ··· 337 343 } 338 344 if (updates.communityLogoUrl !== undefined) { 339 345 dbUpdates.communityLogoUrl = updates.communityLogoUrl 346 + } 347 + if (updates.faviconUrl !== undefined) { 348 + dbUpdates.faviconUrl = updates.faviconUrl 340 349 } 341 350 if (updates.primaryColor !== undefined) { 342 351 dbUpdates.primaryColor = updates.primaryColor
+2
src/validation/admin-settings.ts
··· 26 26 .nullable() 27 27 .optional(), 28 28 communityLogoUrl: z.url('Community logo must be a valid URL').nullable().optional(), 29 + faviconUrl: z.url('Favicon must be a valid URL').nullable().optional(), 29 30 primaryColor: z 30 31 .string() 31 32 .regex(hexColorPattern, 'Primary color must be a valid hex color (e.g., #ff0000)') ··· 68 69 reactionSet: z.array(z.string()), 69 70 communityDescription: z.string().nullable(), 70 71 communityLogoUrl: z.string().nullable(), 72 + faviconUrl: z.string().nullable(), 71 73 primaryColor: z.string().nullable(), 72 74 accentColor: z.string().nullable(), 73 75 jurisdictionCountry: z.string().nullable(),
+1
tests/integration/tenant-isolation.test.ts
··· 41 41 signing_key TEXT, 42 42 rotation_key TEXT, 43 43 community_logo_url TEXT, 44 + favicon_url TEXT, 44 45 primary_color TEXT, 45 46 accent_color TEXT, 46 47 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+469
tests/unit/routes/admin-design.test.ts
··· 1 + /** 2 + * Tests for admin design upload routes (logo + favicon). 3 + * TDD: these tests are written before the implementation. 4 + */ 5 + 6 + import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest' 7 + import Fastify from 'fastify' 8 + import multipart from '@fastify/multipart' 9 + import type { FastifyInstance } from 'fastify' 10 + import type { Env } from '../../../src/config/env.js' 11 + import type { RequestUser } from '../../../src/auth/middleware.js' 12 + import type { SessionService } from '../../../src/auth/session.js' 13 + import type { SetupService } from '../../../src/setup/service.js' 14 + import type { StorageService } from '../../../src/lib/storage.js' 15 + import { type DbChain, createChainableProxy, createMockDb } from '../../helpers/mock-db.js' 16 + 17 + // --------------------------------------------------------------------------- 18 + // Mock sharp -- must be hoisted before route import 19 + // --------------------------------------------------------------------------- 20 + 21 + vi.mock('sharp', () => { 22 + const mockSharpInstance = { 23 + resize: vi.fn().mockReturnThis(), 24 + webp: vi.fn().mockReturnThis(), 25 + toBuffer: vi.fn().mockResolvedValue(Buffer.from('processed-image')), 26 + } 27 + return { 28 + default: vi.fn(() => mockSharpInstance), 29 + __mockInstance: mockSharpInstance, 30 + } 31 + }) 32 + 33 + // Import routes after mocks 34 + import { adminDesignRoutes } from '../../../src/routes/admin-design.js' 35 + 36 + // Retrieve the mock instance exported from the vi.mock factory 37 + const { __mockInstance: mockSharpInstance } = await vi.importMock<{ __mockInstance: { resize: ReturnType<typeof vi.fn>; webp: ReturnType<typeof vi.fn>; toBuffer: ReturnType<typeof vi.fn> } }>('sharp') 38 + 39 + // --------------------------------------------------------------------------- 40 + // Mock env 41 + // --------------------------------------------------------------------------- 42 + 43 + const mockEnv = { 44 + COMMUNITY_DID: 'did:plc:community123', 45 + UPLOAD_MAX_SIZE_BYTES: 5_242_880, 46 + RATE_LIMIT_WRITE: 10, 47 + RATE_LIMIT_READ_ANON: 100, 48 + RATE_LIMIT_READ_AUTH: 300, 49 + } as Env 50 + 51 + // --------------------------------------------------------------------------- 52 + // Test constants 53 + // --------------------------------------------------------------------------- 54 + 55 + const TEST_DID = 'did:plc:testadmin123' 56 + const TEST_HANDLE = 'admin.bsky.team' 57 + const TEST_SID = 'a'.repeat(64) 58 + 59 + // --------------------------------------------------------------------------- 60 + // Mock user builder 61 + // --------------------------------------------------------------------------- 62 + 63 + function testUser(overrides?: Partial<RequestUser>): RequestUser { 64 + return { 65 + did: TEST_DID, 66 + handle: TEST_HANDLE, 67 + sid: TEST_SID, 68 + ...overrides, 69 + } 70 + } 71 + 72 + // --------------------------------------------------------------------------- 73 + // Mock DB 74 + // --------------------------------------------------------------------------- 75 + 76 + const mockDb = createMockDb() 77 + 78 + let updateChain: DbChain 79 + 80 + function resetAllDbMocks(): void { 81 + const selectChain = createChainableProxy([]) 82 + updateChain = createChainableProxy([]) 83 + mockDb.insert.mockReturnValue(createChainableProxy()) 84 + mockDb.select.mockReturnValue(selectChain) 85 + mockDb.update.mockReturnValue(updateChain) 86 + mockDb.delete.mockReturnValue(createChainableProxy()) 87 + } 88 + 89 + // --------------------------------------------------------------------------- 90 + // Mock storage 91 + // --------------------------------------------------------------------------- 92 + 93 + function createMockStorage(): StorageService { 94 + return { 95 + store: vi.fn().mockResolvedValue('http://localhost:3000/uploads/logos/test.webp'), 96 + delete: vi.fn().mockResolvedValue(undefined), 97 + } 98 + } 99 + 100 + // --------------------------------------------------------------------------- 101 + // requireAdmin mock (simulates admin middleware) 102 + // --------------------------------------------------------------------------- 103 + 104 + function createMockRequireAdmin(user?: RequestUser) { 105 + return (request: { user?: RequestUser }, reply: { status: (code: number) => { send: (body: unknown) => void } }, done: () => void) => { 106 + if (!user) { 107 + reply.status(401).send({ error: 'Authentication required' }) 108 + return 109 + } 110 + request.user = user 111 + done() 112 + } 113 + } 114 + 115 + // --------------------------------------------------------------------------- 116 + // Helper: build app with mocked deps 117 + // --------------------------------------------------------------------------- 118 + 119 + async function buildTestApp( 120 + user?: RequestUser, 121 + storageOverride?: StorageService 122 + ): Promise<FastifyInstance> { 123 + const app = Fastify({ logger: false }) 124 + 125 + // Register multipart before routes (required for request.file()) 126 + await app.register(multipart, { 127 + limits: { fileSize: mockEnv.UPLOAD_MAX_SIZE_BYTES }, 128 + }) 129 + 130 + const storage = storageOverride ?? createMockStorage() 131 + 132 + app.decorate('db', mockDb as never) 133 + app.decorate('env', mockEnv) 134 + app.decorate('authMiddleware', {} as never) 135 + app.decorate('requireAdmin', createMockRequireAdmin(user)) 136 + app.decorate('storage', storage) 137 + app.decorate('firehose', {} as never) 138 + app.decorate('oauthClient', {} as never) 139 + app.decorate('sessionService', {} as SessionService) 140 + app.decorate('setupService', {} as SetupService) 141 + app.decorate('cache', {} as never) 142 + app.decorateRequest('user', undefined as RequestUser | undefined) 143 + app.decorateRequest('communityDid', undefined as string | undefined) 144 + app.addHook('onRequest', (request, _reply, done) => { 145 + request.communityDid = mockEnv.COMMUNITY_DID 146 + done() 147 + }) 148 + 149 + await app.register(adminDesignRoutes()) 150 + await app.ready() 151 + 152 + return app 153 + } 154 + 155 + // --------------------------------------------------------------------------- 156 + // Helper: create multipart form body for Fastify inject 157 + // --------------------------------------------------------------------------- 158 + 159 + function createMultipartPayload( 160 + filename: string, 161 + mimetype: string, 162 + data: Buffer 163 + ): { body: string; contentType: string } { 164 + const boundary = `----TestBoundary${String(Date.now())}` 165 + const body = [ 166 + `--${boundary}`, 167 + `Content-Disposition: form-data; name="file"; filename="${filename}"`, 168 + `Content-Type: ${mimetype}`, 169 + '', 170 + data.toString('binary'), 171 + `--${boundary}--`, 172 + ].join('\r\n') 173 + 174 + return { 175 + body, 176 + contentType: `multipart/form-data; boundary=${boundary}`, 177 + } 178 + } 179 + 180 + // =========================================================================== 181 + // Test suite 182 + // =========================================================================== 183 + 184 + describe('admin design routes', () => { 185 + // ========================================================================= 186 + // POST /api/admin/design/logo 187 + // ========================================================================= 188 + 189 + describe('POST /api/admin/design/logo', () => { 190 + let app: FastifyInstance 191 + let mockStorage: StorageService 192 + 193 + beforeAll(async () => { 194 + mockStorage = createMockStorage() 195 + app = await buildTestApp(testUser(), mockStorage) 196 + }) 197 + 198 + afterAll(async () => { 199 + await app.close() 200 + }) 201 + 202 + beforeEach(() => { 203 + vi.clearAllMocks() 204 + resetAllDbMocks() 205 + ;(mockStorage.store as ReturnType<typeof vi.fn>).mockResolvedValue( 206 + 'http://localhost:3000/uploads/logos/test.webp' 207 + ) 208 + }) 209 + 210 + it('uploads logo and returns URL', async () => { 211 + const imageData = Buffer.from('fake-png-data') 212 + const { body, contentType } = createMultipartPayload('logo.png', 'image/png', imageData) 213 + 214 + const response = await app.inject({ 215 + method: 'POST', 216 + url: '/api/admin/design/logo', 217 + headers: { 218 + authorization: 'Bearer test-token', 219 + 'content-type': contentType, 220 + }, 221 + body, 222 + }) 223 + 224 + expect(response.statusCode).toBe(200) 225 + const result = response.json<{ url: string }>() 226 + expect(result.url).toBe('http://localhost:3000/uploads/logos/test.webp') 227 + // eslint-disable-next-line @typescript-eslint/unbound-method 228 + expect(mockStorage.store).toHaveBeenCalledOnce() 229 + expect(mockDb.update).toHaveBeenCalledOnce() 230 + }) 231 + 232 + it('resizes logo to 512x512', async () => { 233 + const imageData = Buffer.from('fake-png-data') 234 + const { body, contentType } = createMultipartPayload('logo.png', 'image/png', imageData) 235 + 236 + await app.inject({ 237 + method: 'POST', 238 + url: '/api/admin/design/logo', 239 + headers: { 240 + authorization: 'Bearer test-token', 241 + 'content-type': contentType, 242 + }, 243 + body, 244 + }) 245 + 246 + expect(mockSharpInstance.resize).toHaveBeenCalledWith(512, 512, { fit: 'cover' }) 247 + expect(mockSharpInstance.webp).toHaveBeenCalledWith({ quality: 85 }) 248 + }) 249 + 250 + it('stores with logos prefix', async () => { 251 + const imageData = Buffer.from('fake-png-data') 252 + const { body, contentType } = createMultipartPayload('logo.png', 'image/png', imageData) 253 + 254 + await app.inject({ 255 + method: 'POST', 256 + url: '/api/admin/design/logo', 257 + headers: { 258 + authorization: 'Bearer test-token', 259 + 'content-type': contentType, 260 + }, 261 + body, 262 + }) 263 + 264 + // eslint-disable-next-line @typescript-eslint/unbound-method 265 + expect(mockStorage.store).toHaveBeenCalledWith( 266 + expect.any(Buffer), 267 + 'image/webp', 268 + 'logos' 269 + ) 270 + }) 271 + 272 + it('returns 401 when not authenticated', async () => { 273 + const noAuthApp = await buildTestApp(undefined) 274 + const imageData = Buffer.from('fake-png-data') 275 + const { body, contentType } = createMultipartPayload('logo.png', 'image/png', imageData) 276 + 277 + const response = await noAuthApp.inject({ 278 + method: 'POST', 279 + url: '/api/admin/design/logo', 280 + headers: { 'content-type': contentType }, 281 + body, 282 + }) 283 + 284 + expect(response.statusCode).toBe(401) 285 + await noAuthApp.close() 286 + }) 287 + 288 + it('returns 400 when no file is uploaded', async () => { 289 + const response = await app.inject({ 290 + method: 'POST', 291 + url: '/api/admin/design/logo', 292 + headers: { 293 + authorization: 'Bearer test-token', 294 + 'content-type': 'multipart/form-data; boundary=----EmptyBoundary', 295 + }, 296 + body: '------EmptyBoundary--\r\n', 297 + }) 298 + 299 + expect(response.statusCode).toBe(400) 300 + }) 301 + 302 + it('returns 400 for invalid MIME type', async () => { 303 + const { body, contentType } = createMultipartPayload( 304 + 'doc.pdf', 305 + 'application/pdf', 306 + Buffer.from('not-an-image') 307 + ) 308 + 309 + const response = await app.inject({ 310 + method: 'POST', 311 + url: '/api/admin/design/logo', 312 + headers: { 313 + authorization: 'Bearer test-token', 314 + 'content-type': contentType, 315 + }, 316 + body, 317 + }) 318 + 319 + expect(response.statusCode).toBe(400) 320 + }) 321 + 322 + it('accepts JPEG files', async () => { 323 + const { body, contentType } = createMultipartPayload( 324 + 'logo.jpg', 325 + 'image/jpeg', 326 + Buffer.from('jpeg-data') 327 + ) 328 + 329 + const response = await app.inject({ 330 + method: 'POST', 331 + url: '/api/admin/design/logo', 332 + headers: { 333 + authorization: 'Bearer test-token', 334 + 'content-type': contentType, 335 + }, 336 + body, 337 + }) 338 + 339 + expect(response.statusCode).toBe(200) 340 + }) 341 + }) 342 + 343 + // ========================================================================= 344 + // POST /api/admin/design/favicon 345 + // ========================================================================= 346 + 347 + describe('POST /api/admin/design/favicon', () => { 348 + let app: FastifyInstance 349 + let mockStorage: StorageService 350 + 351 + beforeAll(async () => { 352 + mockStorage = createMockStorage() 353 + ;(mockStorage.store as ReturnType<typeof vi.fn>).mockResolvedValue( 354 + 'http://localhost:3000/uploads/favicons/test.webp' 355 + ) 356 + app = await buildTestApp(testUser(), mockStorage) 357 + }) 358 + 359 + afterAll(async () => { 360 + await app.close() 361 + }) 362 + 363 + beforeEach(() => { 364 + vi.clearAllMocks() 365 + resetAllDbMocks() 366 + ;(mockStorage.store as ReturnType<typeof vi.fn>).mockResolvedValue( 367 + 'http://localhost:3000/uploads/favicons/test.webp' 368 + ) 369 + }) 370 + 371 + it('uploads favicon and returns URL', async () => { 372 + const imageData = Buffer.from('fake-png-data') 373 + const { body, contentType } = createMultipartPayload('favicon.png', 'image/png', imageData) 374 + 375 + const response = await app.inject({ 376 + method: 'POST', 377 + url: '/api/admin/design/favicon', 378 + headers: { 379 + authorization: 'Bearer test-token', 380 + 'content-type': contentType, 381 + }, 382 + body, 383 + }) 384 + 385 + expect(response.statusCode).toBe(200) 386 + const result = response.json<{ url: string }>() 387 + expect(result.url).toBe('http://localhost:3000/uploads/favicons/test.webp') 388 + // eslint-disable-next-line @typescript-eslint/unbound-method 389 + expect(mockStorage.store).toHaveBeenCalledOnce() 390 + expect(mockDb.update).toHaveBeenCalledOnce() 391 + }) 392 + 393 + it('resizes favicon to 256x256', async () => { 394 + const imageData = Buffer.from('fake-png-data') 395 + const { body, contentType } = createMultipartPayload('favicon.png', 'image/png', imageData) 396 + 397 + await app.inject({ 398 + method: 'POST', 399 + url: '/api/admin/design/favicon', 400 + headers: { 401 + authorization: 'Bearer test-token', 402 + 'content-type': contentType, 403 + }, 404 + body, 405 + }) 406 + 407 + expect(mockSharpInstance.resize).toHaveBeenCalledWith(256, 256, { fit: 'cover' }) 408 + expect(mockSharpInstance.webp).toHaveBeenCalledWith({ quality: 90 }) 409 + }) 410 + 411 + it('stores with favicons prefix', async () => { 412 + const imageData = Buffer.from('fake-png-data') 413 + const { body, contentType } = createMultipartPayload('favicon.png', 'image/png', imageData) 414 + 415 + await app.inject({ 416 + method: 'POST', 417 + url: '/api/admin/design/favicon', 418 + headers: { 419 + authorization: 'Bearer test-token', 420 + 'content-type': contentType, 421 + }, 422 + body, 423 + }) 424 + 425 + // eslint-disable-next-line @typescript-eslint/unbound-method 426 + expect(mockStorage.store).toHaveBeenCalledWith( 427 + expect.any(Buffer), 428 + 'image/webp', 429 + 'favicons' 430 + ) 431 + }) 432 + 433 + it('returns 401 when not authenticated', async () => { 434 + const noAuthApp = await buildTestApp(undefined) 435 + const imageData = Buffer.from('fake-png-data') 436 + const { body, contentType } = createMultipartPayload('favicon.png', 'image/png', imageData) 437 + 438 + const response = await noAuthApp.inject({ 439 + method: 'POST', 440 + url: '/api/admin/design/favicon', 441 + headers: { 'content-type': contentType }, 442 + body, 443 + }) 444 + 445 + expect(response.statusCode).toBe(401) 446 + await noAuthApp.close() 447 + }) 448 + 449 + it('returns 400 for invalid MIME type', async () => { 450 + const { body, contentType } = createMultipartPayload( 451 + 'doc.txt', 452 + 'text/plain', 453 + Buffer.from('not-an-image') 454 + ) 455 + 456 + const response = await app.inject({ 457 + method: 'POST', 458 + url: '/api/admin/design/favicon', 459 + headers: { 460 + authorization: 'Bearer test-token', 461 + 'content-type': contentType, 462 + }, 463 + body, 464 + }) 465 + 466 + expect(response.statusCode).toBe(400) 467 + }) 468 + }) 469 + })