Barazo AppView backend barazo.forum

feat(topics): AT Protocol-style lookup endpoints (#139)

Add author-scoped endpoints for topics and replies to support
AT Protocol-style URLs (/{handle}/{rkey}) and fix the latent
rkey collision bug in the existing by-rkey endpoint.

- Add GET /api/topics/by-author-rkey/:handle/:rkey
- Add GET /api/replies/by-author-rkey/:handle/:rkey
- Add shared resolveHandleToDid utility (local DB + Bluesky fallback)
- Add authorHandle to POST /api/topics 201 response
- Enrich GET /api/notifications with actorHandle, subjectTitle,
subjectAuthorDid, subjectAuthorHandle, and message fields
- Update cross-post URL builder to AT Protocol-style format
- Add composite indexes on (author_did, rkey) for topics and replies
- Keep existing by-rkey endpoint for backwards compatibility

authored by

Guido X Jansen and committed by
GitHub
b8502ee8 a3bb0186

+4652 -14
+2
drizzle/0008_add_author_rkey_indexes.sql
··· 1 + CREATE INDEX "topics_author_did_rkey_idx" ON "topics" USING btree ("author_did","rkey");--> statement-breakpoint 2 + CREATE INDEX "replies_author_did_rkey_idx" ON "replies" USING btree ("author_did","rkey");
+4113
drizzle/meta/0008_snapshot.json
··· 1 + { 2 + "id": "d168c26e-76b0-40aa-b1b3-81eeba8a99c8", 3 + "prevId": "30af658d-1dce-41a5-8c85-ed9d81fa33dd", 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 + "topics_author_did_rkey_idx": { 513 + "name": "topics_author_did_rkey_idx", 514 + "columns": [ 515 + { 516 + "expression": "author_did", 517 + "isExpression": false, 518 + "asc": true, 519 + "nulls": "last" 520 + }, 521 + { 522 + "expression": "rkey", 523 + "isExpression": false, 524 + "asc": true, 525 + "nulls": "last" 526 + } 527 + ], 528 + "isUnique": false, 529 + "concurrently": false, 530 + "method": "btree", 531 + "with": {} 532 + } 533 + }, 534 + "foreignKeys": {}, 535 + "compositePrimaryKeys": {}, 536 + "uniqueConstraints": {}, 537 + "policies": { 538 + "tenant_isolation": { 539 + "name": "tenant_isolation", 540 + "as": "PERMISSIVE", 541 + "for": "ALL", 542 + "to": [ 543 + "barazo_app" 544 + ], 545 + "using": "community_did = current_setting('app.current_community_did', true)", 546 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 547 + } 548 + }, 549 + "checkConstraints": {}, 550 + "isRLSEnabled": true 551 + }, 552 + "public.replies": { 553 + "name": "replies", 554 + "schema": "", 555 + "columns": { 556 + "uri": { 557 + "name": "uri", 558 + "type": "text", 559 + "primaryKey": true, 560 + "notNull": true 561 + }, 562 + "rkey": { 563 + "name": "rkey", 564 + "type": "text", 565 + "primaryKey": false, 566 + "notNull": true 567 + }, 568 + "author_did": { 569 + "name": "author_did", 570 + "type": "text", 571 + "primaryKey": false, 572 + "notNull": true 573 + }, 574 + "content": { 575 + "name": "content", 576 + "type": "text", 577 + "primaryKey": false, 578 + "notNull": true 579 + }, 580 + "content_format": { 581 + "name": "content_format", 582 + "type": "text", 583 + "primaryKey": false, 584 + "notNull": false 585 + }, 586 + "root_uri": { 587 + "name": "root_uri", 588 + "type": "text", 589 + "primaryKey": false, 590 + "notNull": true 591 + }, 592 + "root_cid": { 593 + "name": "root_cid", 594 + "type": "text", 595 + "primaryKey": false, 596 + "notNull": true 597 + }, 598 + "parent_uri": { 599 + "name": "parent_uri", 600 + "type": "text", 601 + "primaryKey": false, 602 + "notNull": true 603 + }, 604 + "parent_cid": { 605 + "name": "parent_cid", 606 + "type": "text", 607 + "primaryKey": false, 608 + "notNull": true 609 + }, 610 + "community_did": { 611 + "name": "community_did", 612 + "type": "text", 613 + "primaryKey": false, 614 + "notNull": true 615 + }, 616 + "cid": { 617 + "name": "cid", 618 + "type": "text", 619 + "primaryKey": false, 620 + "notNull": true 621 + }, 622 + "labels": { 623 + "name": "labels", 624 + "type": "jsonb", 625 + "primaryKey": false, 626 + "notNull": false 627 + }, 628 + "reaction_count": { 629 + "name": "reaction_count", 630 + "type": "integer", 631 + "primaryKey": false, 632 + "notNull": true, 633 + "default": 0 634 + }, 635 + "vote_count": { 636 + "name": "vote_count", 637 + "type": "integer", 638 + "primaryKey": false, 639 + "notNull": true, 640 + "default": 0 641 + }, 642 + "depth": { 643 + "name": "depth", 644 + "type": "integer", 645 + "primaryKey": false, 646 + "notNull": true, 647 + "default": 1 648 + }, 649 + "created_at": { 650 + "name": "created_at", 651 + "type": "timestamp with time zone", 652 + "primaryKey": false, 653 + "notNull": true 654 + }, 655 + "indexed_at": { 656 + "name": "indexed_at", 657 + "type": "timestamp with time zone", 658 + "primaryKey": false, 659 + "notNull": true, 660 + "default": "now()" 661 + }, 662 + "is_author_deleted": { 663 + "name": "is_author_deleted", 664 + "type": "boolean", 665 + "primaryKey": false, 666 + "notNull": true, 667 + "default": false 668 + }, 669 + "is_mod_deleted": { 670 + "name": "is_mod_deleted", 671 + "type": "boolean", 672 + "primaryKey": false, 673 + "notNull": true, 674 + "default": false 675 + }, 676 + "moderation_status": { 677 + "name": "moderation_status", 678 + "type": "text", 679 + "primaryKey": false, 680 + "notNull": true, 681 + "default": "'approved'" 682 + }, 683 + "trust_status": { 684 + "name": "trust_status", 685 + "type": "text", 686 + "primaryKey": false, 687 + "notNull": true, 688 + "default": "'trusted'" 689 + } 690 + }, 691 + "indexes": { 692 + "replies_author_did_idx": { 693 + "name": "replies_author_did_idx", 694 + "columns": [ 695 + { 696 + "expression": "author_did", 697 + "isExpression": false, 698 + "asc": true, 699 + "nulls": "last" 700 + } 701 + ], 702 + "isUnique": false, 703 + "concurrently": false, 704 + "method": "btree", 705 + "with": {} 706 + }, 707 + "replies_root_uri_idx": { 708 + "name": "replies_root_uri_idx", 709 + "columns": [ 710 + { 711 + "expression": "root_uri", 712 + "isExpression": false, 713 + "asc": true, 714 + "nulls": "last" 715 + } 716 + ], 717 + "isUnique": false, 718 + "concurrently": false, 719 + "method": "btree", 720 + "with": {} 721 + }, 722 + "replies_parent_uri_idx": { 723 + "name": "replies_parent_uri_idx", 724 + "columns": [ 725 + { 726 + "expression": "parent_uri", 727 + "isExpression": false, 728 + "asc": true, 729 + "nulls": "last" 730 + } 731 + ], 732 + "isUnique": false, 733 + "concurrently": false, 734 + "method": "btree", 735 + "with": {} 736 + }, 737 + "replies_created_at_idx": { 738 + "name": "replies_created_at_idx", 739 + "columns": [ 740 + { 741 + "expression": "created_at", 742 + "isExpression": false, 743 + "asc": true, 744 + "nulls": "last" 745 + } 746 + ], 747 + "isUnique": false, 748 + "concurrently": false, 749 + "method": "btree", 750 + "with": {} 751 + }, 752 + "replies_community_did_idx": { 753 + "name": "replies_community_did_idx", 754 + "columns": [ 755 + { 756 + "expression": "community_did", 757 + "isExpression": false, 758 + "asc": true, 759 + "nulls": "last" 760 + } 761 + ], 762 + "isUnique": false, 763 + "concurrently": false, 764 + "method": "btree", 765 + "with": {} 766 + }, 767 + "replies_moderation_status_idx": { 768 + "name": "replies_moderation_status_idx", 769 + "columns": [ 770 + { 771 + "expression": "moderation_status", 772 + "isExpression": false, 773 + "asc": true, 774 + "nulls": "last" 775 + } 776 + ], 777 + "isUnique": false, 778 + "concurrently": false, 779 + "method": "btree", 780 + "with": {} 781 + }, 782 + "replies_trust_status_idx": { 783 + "name": "replies_trust_status_idx", 784 + "columns": [ 785 + { 786 + "expression": "trust_status", 787 + "isExpression": false, 788 + "asc": true, 789 + "nulls": "last" 790 + } 791 + ], 792 + "isUnique": false, 793 + "concurrently": false, 794 + "method": "btree", 795 + "with": {} 796 + }, 797 + "replies_root_uri_created_at_idx": { 798 + "name": "replies_root_uri_created_at_idx", 799 + "columns": [ 800 + { 801 + "expression": "root_uri", 802 + "isExpression": false, 803 + "asc": true, 804 + "nulls": "last" 805 + }, 806 + { 807 + "expression": "created_at", 808 + "isExpression": false, 809 + "asc": true, 810 + "nulls": "last" 811 + } 812 + ], 813 + "isUnique": false, 814 + "concurrently": false, 815 + "method": "btree", 816 + "with": {} 817 + }, 818 + "replies_root_uri_depth_idx": { 819 + "name": "replies_root_uri_depth_idx", 820 + "columns": [ 821 + { 822 + "expression": "root_uri", 823 + "isExpression": false, 824 + "asc": true, 825 + "nulls": "last" 826 + }, 827 + { 828 + "expression": "depth", 829 + "isExpression": false, 830 + "asc": true, 831 + "nulls": "last" 832 + } 833 + ], 834 + "isUnique": false, 835 + "concurrently": false, 836 + "method": "btree", 837 + "with": {} 838 + }, 839 + "replies_author_did_rkey_idx": { 840 + "name": "replies_author_did_rkey_idx", 841 + "columns": [ 842 + { 843 + "expression": "author_did", 844 + "isExpression": false, 845 + "asc": true, 846 + "nulls": "last" 847 + }, 848 + { 849 + "expression": "rkey", 850 + "isExpression": false, 851 + "asc": true, 852 + "nulls": "last" 853 + } 854 + ], 855 + "isUnique": false, 856 + "concurrently": false, 857 + "method": "btree", 858 + "with": {} 859 + } 860 + }, 861 + "foreignKeys": {}, 862 + "compositePrimaryKeys": {}, 863 + "uniqueConstraints": {}, 864 + "policies": { 865 + "tenant_isolation": { 866 + "name": "tenant_isolation", 867 + "as": "PERMISSIVE", 868 + "for": "ALL", 869 + "to": [ 870 + "barazo_app" 871 + ], 872 + "using": "community_did = current_setting('app.current_community_did', true)", 873 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 874 + } 875 + }, 876 + "checkConstraints": {}, 877 + "isRLSEnabled": true 878 + }, 879 + "public.reactions": { 880 + "name": "reactions", 881 + "schema": "", 882 + "columns": { 883 + "uri": { 884 + "name": "uri", 885 + "type": "text", 886 + "primaryKey": true, 887 + "notNull": true 888 + }, 889 + "rkey": { 890 + "name": "rkey", 891 + "type": "text", 892 + "primaryKey": false, 893 + "notNull": true 894 + }, 895 + "author_did": { 896 + "name": "author_did", 897 + "type": "text", 898 + "primaryKey": false, 899 + "notNull": true 900 + }, 901 + "subject_uri": { 902 + "name": "subject_uri", 903 + "type": "text", 904 + "primaryKey": false, 905 + "notNull": true 906 + }, 907 + "subject_cid": { 908 + "name": "subject_cid", 909 + "type": "text", 910 + "primaryKey": false, 911 + "notNull": true 912 + }, 913 + "type": { 914 + "name": "type", 915 + "type": "text", 916 + "primaryKey": false, 917 + "notNull": true 918 + }, 919 + "community_did": { 920 + "name": "community_did", 921 + "type": "text", 922 + "primaryKey": false, 923 + "notNull": true 924 + }, 925 + "cid": { 926 + "name": "cid", 927 + "type": "text", 928 + "primaryKey": false, 929 + "notNull": true 930 + }, 931 + "created_at": { 932 + "name": "created_at", 933 + "type": "timestamp with time zone", 934 + "primaryKey": false, 935 + "notNull": true 936 + }, 937 + "indexed_at": { 938 + "name": "indexed_at", 939 + "type": "timestamp with time zone", 940 + "primaryKey": false, 941 + "notNull": true, 942 + "default": "now()" 943 + } 944 + }, 945 + "indexes": { 946 + "reactions_author_did_idx": { 947 + "name": "reactions_author_did_idx", 948 + "columns": [ 949 + { 950 + "expression": "author_did", 951 + "isExpression": false, 952 + "asc": true, 953 + "nulls": "last" 954 + } 955 + ], 956 + "isUnique": false, 957 + "concurrently": false, 958 + "method": "btree", 959 + "with": {} 960 + }, 961 + "reactions_subject_uri_idx": { 962 + "name": "reactions_subject_uri_idx", 963 + "columns": [ 964 + { 965 + "expression": "subject_uri", 966 + "isExpression": false, 967 + "asc": true, 968 + "nulls": "last" 969 + } 970 + ], 971 + "isUnique": false, 972 + "concurrently": false, 973 + "method": "btree", 974 + "with": {} 975 + }, 976 + "reactions_community_did_idx": { 977 + "name": "reactions_community_did_idx", 978 + "columns": [ 979 + { 980 + "expression": "community_did", 981 + "isExpression": false, 982 + "asc": true, 983 + "nulls": "last" 984 + } 985 + ], 986 + "isUnique": false, 987 + "concurrently": false, 988 + "method": "btree", 989 + "with": {} 990 + }, 991 + "reactions_subject_uri_type_idx": { 992 + "name": "reactions_subject_uri_type_idx", 993 + "columns": [ 994 + { 995 + "expression": "subject_uri", 996 + "isExpression": false, 997 + "asc": true, 998 + "nulls": "last" 999 + }, 1000 + { 1001 + "expression": "type", 1002 + "isExpression": false, 1003 + "asc": true, 1004 + "nulls": "last" 1005 + } 1006 + ], 1007 + "isUnique": false, 1008 + "concurrently": false, 1009 + "method": "btree", 1010 + "with": {} 1011 + } 1012 + }, 1013 + "foreignKeys": {}, 1014 + "compositePrimaryKeys": {}, 1015 + "uniqueConstraints": { 1016 + "reactions_author_subject_type_uniq": { 1017 + "name": "reactions_author_subject_type_uniq", 1018 + "nullsNotDistinct": false, 1019 + "columns": [ 1020 + "author_did", 1021 + "subject_uri", 1022 + "type" 1023 + ] 1024 + } 1025 + }, 1026 + "policies": { 1027 + "tenant_isolation": { 1028 + "name": "tenant_isolation", 1029 + "as": "PERMISSIVE", 1030 + "for": "ALL", 1031 + "to": [ 1032 + "barazo_app" 1033 + ], 1034 + "using": "community_did = current_setting('app.current_community_did', true)", 1035 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1036 + } 1037 + }, 1038 + "checkConstraints": {}, 1039 + "isRLSEnabled": true 1040 + }, 1041 + "public.votes": { 1042 + "name": "votes", 1043 + "schema": "", 1044 + "columns": { 1045 + "uri": { 1046 + "name": "uri", 1047 + "type": "text", 1048 + "primaryKey": true, 1049 + "notNull": true 1050 + }, 1051 + "rkey": { 1052 + "name": "rkey", 1053 + "type": "text", 1054 + "primaryKey": false, 1055 + "notNull": true 1056 + }, 1057 + "author_did": { 1058 + "name": "author_did", 1059 + "type": "text", 1060 + "primaryKey": false, 1061 + "notNull": true 1062 + }, 1063 + "subject_uri": { 1064 + "name": "subject_uri", 1065 + "type": "text", 1066 + "primaryKey": false, 1067 + "notNull": true 1068 + }, 1069 + "subject_cid": { 1070 + "name": "subject_cid", 1071 + "type": "text", 1072 + "primaryKey": false, 1073 + "notNull": true 1074 + }, 1075 + "direction": { 1076 + "name": "direction", 1077 + "type": "text", 1078 + "primaryKey": false, 1079 + "notNull": true 1080 + }, 1081 + "community_did": { 1082 + "name": "community_did", 1083 + "type": "text", 1084 + "primaryKey": false, 1085 + "notNull": true 1086 + }, 1087 + "cid": { 1088 + "name": "cid", 1089 + "type": "text", 1090 + "primaryKey": false, 1091 + "notNull": true 1092 + }, 1093 + "created_at": { 1094 + "name": "created_at", 1095 + "type": "timestamp with time zone", 1096 + "primaryKey": false, 1097 + "notNull": true 1098 + }, 1099 + "indexed_at": { 1100 + "name": "indexed_at", 1101 + "type": "timestamp with time zone", 1102 + "primaryKey": false, 1103 + "notNull": true, 1104 + "default": "now()" 1105 + } 1106 + }, 1107 + "indexes": { 1108 + "votes_author_did_idx": { 1109 + "name": "votes_author_did_idx", 1110 + "columns": [ 1111 + { 1112 + "expression": "author_did", 1113 + "isExpression": false, 1114 + "asc": true, 1115 + "nulls": "last" 1116 + } 1117 + ], 1118 + "isUnique": false, 1119 + "concurrently": false, 1120 + "method": "btree", 1121 + "with": {} 1122 + }, 1123 + "votes_subject_uri_idx": { 1124 + "name": "votes_subject_uri_idx", 1125 + "columns": [ 1126 + { 1127 + "expression": "subject_uri", 1128 + "isExpression": false, 1129 + "asc": true, 1130 + "nulls": "last" 1131 + } 1132 + ], 1133 + "isUnique": false, 1134 + "concurrently": false, 1135 + "method": "btree", 1136 + "with": {} 1137 + }, 1138 + "votes_community_did_idx": { 1139 + "name": "votes_community_did_idx", 1140 + "columns": [ 1141 + { 1142 + "expression": "community_did", 1143 + "isExpression": false, 1144 + "asc": true, 1145 + "nulls": "last" 1146 + } 1147 + ], 1148 + "isUnique": false, 1149 + "concurrently": false, 1150 + "method": "btree", 1151 + "with": {} 1152 + } 1153 + }, 1154 + "foreignKeys": {}, 1155 + "compositePrimaryKeys": {}, 1156 + "uniqueConstraints": { 1157 + "votes_author_subject_uniq": { 1158 + "name": "votes_author_subject_uniq", 1159 + "nullsNotDistinct": false, 1160 + "columns": [ 1161 + "author_did", 1162 + "subject_uri" 1163 + ] 1164 + } 1165 + }, 1166 + "policies": {}, 1167 + "checkConstraints": {}, 1168 + "isRLSEnabled": false 1169 + }, 1170 + "public.tracked_repos": { 1171 + "name": "tracked_repos", 1172 + "schema": "", 1173 + "columns": { 1174 + "did": { 1175 + "name": "did", 1176 + "type": "text", 1177 + "primaryKey": true, 1178 + "notNull": true 1179 + }, 1180 + "tracked_at": { 1181 + "name": "tracked_at", 1182 + "type": "timestamp with time zone", 1183 + "primaryKey": false, 1184 + "notNull": true, 1185 + "default": "now()" 1186 + } 1187 + }, 1188 + "indexes": {}, 1189 + "foreignKeys": {}, 1190 + "compositePrimaryKeys": {}, 1191 + "uniqueConstraints": {}, 1192 + "policies": {}, 1193 + "checkConstraints": {}, 1194 + "isRLSEnabled": false 1195 + }, 1196 + "public.community_settings": { 1197 + "name": "community_settings", 1198 + "schema": "", 1199 + "columns": { 1200 + "community_did": { 1201 + "name": "community_did", 1202 + "type": "text", 1203 + "primaryKey": true, 1204 + "notNull": true 1205 + }, 1206 + "domains": { 1207 + "name": "domains", 1208 + "type": "jsonb", 1209 + "primaryKey": false, 1210 + "notNull": true, 1211 + "default": "'[]'::jsonb" 1212 + }, 1213 + "initialized": { 1214 + "name": "initialized", 1215 + "type": "boolean", 1216 + "primaryKey": false, 1217 + "notNull": true, 1218 + "default": false 1219 + }, 1220 + "admin_did": { 1221 + "name": "admin_did", 1222 + "type": "text", 1223 + "primaryKey": false, 1224 + "notNull": false 1225 + }, 1226 + "community_name": { 1227 + "name": "community_name", 1228 + "type": "text", 1229 + "primaryKey": false, 1230 + "notNull": true, 1231 + "default": "'Barazo Community'" 1232 + }, 1233 + "maturity_rating": { 1234 + "name": "maturity_rating", 1235 + "type": "text", 1236 + "primaryKey": false, 1237 + "notNull": true, 1238 + "default": "'safe'" 1239 + }, 1240 + "reaction_set": { 1241 + "name": "reaction_set", 1242 + "type": "jsonb", 1243 + "primaryKey": false, 1244 + "notNull": true, 1245 + "default": "'[\"like\"]'::jsonb" 1246 + }, 1247 + "moderation_thresholds": { 1248 + "name": "moderation_thresholds", 1249 + "type": "jsonb", 1250 + "primaryKey": false, 1251 + "notNull": true, 1252 + "default": "'{\"autoBlockReportCount\":5,\"warnThreshold\":3,\"firstPostQueueCount\":0,\"newAccountDays\":7,\"newAccountWriteRatePerMin\":3,\"establishedWriteRatePerMin\":10,\"linkHoldEnabled\":false,\"topicCreationDelayEnabled\":false,\"burstPostCount\":5,\"burstWindowMinutes\":10,\"trustedPostThreshold\":10}'::jsonb" 1253 + }, 1254 + "word_filter": { 1255 + "name": "word_filter", 1256 + "type": "jsonb", 1257 + "primaryKey": false, 1258 + "notNull": true, 1259 + "default": "'[]'::jsonb" 1260 + }, 1261 + "jurisdiction_country": { 1262 + "name": "jurisdiction_country", 1263 + "type": "text", 1264 + "primaryKey": false, 1265 + "notNull": false 1266 + }, 1267 + "age_threshold": { 1268 + "name": "age_threshold", 1269 + "type": "integer", 1270 + "primaryKey": false, 1271 + "notNull": true, 1272 + "default": 16 1273 + }, 1274 + "max_reply_depth": { 1275 + "name": "max_reply_depth", 1276 + "type": "integer", 1277 + "primaryKey": false, 1278 + "notNull": true, 1279 + "default": 9999 1280 + }, 1281 + "require_login_for_mature": { 1282 + "name": "require_login_for_mature", 1283 + "type": "boolean", 1284 + "primaryKey": false, 1285 + "notNull": true, 1286 + "default": true 1287 + }, 1288 + "community_description": { 1289 + "name": "community_description", 1290 + "type": "text", 1291 + "primaryKey": false, 1292 + "notNull": false 1293 + }, 1294 + "handle": { 1295 + "name": "handle", 1296 + "type": "text", 1297 + "primaryKey": false, 1298 + "notNull": false 1299 + }, 1300 + "service_endpoint": { 1301 + "name": "service_endpoint", 1302 + "type": "text", 1303 + "primaryKey": false, 1304 + "notNull": false 1305 + }, 1306 + "signing_key": { 1307 + "name": "signing_key", 1308 + "type": "text", 1309 + "primaryKey": false, 1310 + "notNull": false 1311 + }, 1312 + "rotation_key": { 1313 + "name": "rotation_key", 1314 + "type": "text", 1315 + "primaryKey": false, 1316 + "notNull": false 1317 + }, 1318 + "community_logo_url": { 1319 + "name": "community_logo_url", 1320 + "type": "text", 1321 + "primaryKey": false, 1322 + "notNull": false 1323 + }, 1324 + "favicon_url": { 1325 + "name": "favicon_url", 1326 + "type": "text", 1327 + "primaryKey": false, 1328 + "notNull": false 1329 + }, 1330 + "header_logo_url": { 1331 + "name": "header_logo_url", 1332 + "type": "text", 1333 + "primaryKey": false, 1334 + "notNull": false 1335 + }, 1336 + "show_community_name": { 1337 + "name": "show_community_name", 1338 + "type": "boolean", 1339 + "primaryKey": false, 1340 + "notNull": true, 1341 + "default": true 1342 + }, 1343 + "primary_color": { 1344 + "name": "primary_color", 1345 + "type": "text", 1346 + "primaryKey": false, 1347 + "notNull": false 1348 + }, 1349 + "accent_color": { 1350 + "name": "accent_color", 1351 + "type": "text", 1352 + "primaryKey": false, 1353 + "notNull": false 1354 + }, 1355 + "created_at": { 1356 + "name": "created_at", 1357 + "type": "timestamp with time zone", 1358 + "primaryKey": false, 1359 + "notNull": true, 1360 + "default": "now()" 1361 + }, 1362 + "updated_at": { 1363 + "name": "updated_at", 1364 + "type": "timestamp with time zone", 1365 + "primaryKey": false, 1366 + "notNull": true, 1367 + "default": "now()" 1368 + } 1369 + }, 1370 + "indexes": {}, 1371 + "foreignKeys": {}, 1372 + "compositePrimaryKeys": {}, 1373 + "uniqueConstraints": {}, 1374 + "policies": { 1375 + "tenant_isolation": { 1376 + "name": "tenant_isolation", 1377 + "as": "PERMISSIVE", 1378 + "for": "ALL", 1379 + "to": [ 1380 + "barazo_app" 1381 + ], 1382 + "using": "community_did = current_setting('app.current_community_did', true)", 1383 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1384 + } 1385 + }, 1386 + "checkConstraints": {}, 1387 + "isRLSEnabled": true 1388 + }, 1389 + "public.categories": { 1390 + "name": "categories", 1391 + "schema": "", 1392 + "columns": { 1393 + "id": { 1394 + "name": "id", 1395 + "type": "text", 1396 + "primaryKey": true, 1397 + "notNull": true 1398 + }, 1399 + "slug": { 1400 + "name": "slug", 1401 + "type": "text", 1402 + "primaryKey": false, 1403 + "notNull": true 1404 + }, 1405 + "name": { 1406 + "name": "name", 1407 + "type": "text", 1408 + "primaryKey": false, 1409 + "notNull": true 1410 + }, 1411 + "description": { 1412 + "name": "description", 1413 + "type": "text", 1414 + "primaryKey": false, 1415 + "notNull": false 1416 + }, 1417 + "parent_id": { 1418 + "name": "parent_id", 1419 + "type": "text", 1420 + "primaryKey": false, 1421 + "notNull": false 1422 + }, 1423 + "sort_order": { 1424 + "name": "sort_order", 1425 + "type": "integer", 1426 + "primaryKey": false, 1427 + "notNull": true, 1428 + "default": 0 1429 + }, 1430 + "community_did": { 1431 + "name": "community_did", 1432 + "type": "text", 1433 + "primaryKey": false, 1434 + "notNull": true 1435 + }, 1436 + "maturity_rating": { 1437 + "name": "maturity_rating", 1438 + "type": "text", 1439 + "primaryKey": false, 1440 + "notNull": true, 1441 + "default": "'safe'" 1442 + }, 1443 + "created_at": { 1444 + "name": "created_at", 1445 + "type": "timestamp with time zone", 1446 + "primaryKey": false, 1447 + "notNull": true, 1448 + "default": "now()" 1449 + }, 1450 + "updated_at": { 1451 + "name": "updated_at", 1452 + "type": "timestamp with time zone", 1453 + "primaryKey": false, 1454 + "notNull": true, 1455 + "default": "now()" 1456 + } 1457 + }, 1458 + "indexes": { 1459 + "categories_slug_community_did_idx": { 1460 + "name": "categories_slug_community_did_idx", 1461 + "columns": [ 1462 + { 1463 + "expression": "slug", 1464 + "isExpression": false, 1465 + "asc": true, 1466 + "nulls": "last" 1467 + }, 1468 + { 1469 + "expression": "community_did", 1470 + "isExpression": false, 1471 + "asc": true, 1472 + "nulls": "last" 1473 + } 1474 + ], 1475 + "isUnique": true, 1476 + "concurrently": false, 1477 + "method": "btree", 1478 + "with": {} 1479 + }, 1480 + "categories_parent_id_idx": { 1481 + "name": "categories_parent_id_idx", 1482 + "columns": [ 1483 + { 1484 + "expression": "parent_id", 1485 + "isExpression": false, 1486 + "asc": true, 1487 + "nulls": "last" 1488 + } 1489 + ], 1490 + "isUnique": false, 1491 + "concurrently": false, 1492 + "method": "btree", 1493 + "with": {} 1494 + }, 1495 + "categories_community_did_idx": { 1496 + "name": "categories_community_did_idx", 1497 + "columns": [ 1498 + { 1499 + "expression": "community_did", 1500 + "isExpression": false, 1501 + "asc": true, 1502 + "nulls": "last" 1503 + } 1504 + ], 1505 + "isUnique": false, 1506 + "concurrently": false, 1507 + "method": "btree", 1508 + "with": {} 1509 + }, 1510 + "categories_maturity_rating_idx": { 1511 + "name": "categories_maturity_rating_idx", 1512 + "columns": [ 1513 + { 1514 + "expression": "maturity_rating", 1515 + "isExpression": false, 1516 + "asc": true, 1517 + "nulls": "last" 1518 + } 1519 + ], 1520 + "isUnique": false, 1521 + "concurrently": false, 1522 + "method": "btree", 1523 + "with": {} 1524 + } 1525 + }, 1526 + "foreignKeys": { 1527 + "categories_parent_id_fk": { 1528 + "name": "categories_parent_id_fk", 1529 + "tableFrom": "categories", 1530 + "tableTo": "categories", 1531 + "columnsFrom": [ 1532 + "parent_id" 1533 + ], 1534 + "columnsTo": [ 1535 + "id" 1536 + ], 1537 + "onDelete": "set null", 1538 + "onUpdate": "no action" 1539 + } 1540 + }, 1541 + "compositePrimaryKeys": {}, 1542 + "uniqueConstraints": {}, 1543 + "policies": { 1544 + "tenant_isolation": { 1545 + "name": "tenant_isolation", 1546 + "as": "PERMISSIVE", 1547 + "for": "ALL", 1548 + "to": [ 1549 + "barazo_app" 1550 + ], 1551 + "using": "community_did = current_setting('app.current_community_did', true)", 1552 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1553 + } 1554 + }, 1555 + "checkConstraints": {}, 1556 + "isRLSEnabled": true 1557 + }, 1558 + "public.moderation_actions": { 1559 + "name": "moderation_actions", 1560 + "schema": "", 1561 + "columns": { 1562 + "id": { 1563 + "name": "id", 1564 + "type": "serial", 1565 + "primaryKey": true, 1566 + "notNull": true 1567 + }, 1568 + "action": { 1569 + "name": "action", 1570 + "type": "text", 1571 + "primaryKey": false, 1572 + "notNull": true 1573 + }, 1574 + "target_uri": { 1575 + "name": "target_uri", 1576 + "type": "text", 1577 + "primaryKey": false, 1578 + "notNull": false 1579 + }, 1580 + "target_did": { 1581 + "name": "target_did", 1582 + "type": "text", 1583 + "primaryKey": false, 1584 + "notNull": false 1585 + }, 1586 + "moderator_did": { 1587 + "name": "moderator_did", 1588 + "type": "text", 1589 + "primaryKey": false, 1590 + "notNull": true 1591 + }, 1592 + "community_did": { 1593 + "name": "community_did", 1594 + "type": "text", 1595 + "primaryKey": false, 1596 + "notNull": true 1597 + }, 1598 + "reason": { 1599 + "name": "reason", 1600 + "type": "text", 1601 + "primaryKey": false, 1602 + "notNull": false 1603 + }, 1604 + "created_at": { 1605 + "name": "created_at", 1606 + "type": "timestamp with time zone", 1607 + "primaryKey": false, 1608 + "notNull": true, 1609 + "default": "now()" 1610 + } 1611 + }, 1612 + "indexes": { 1613 + "mod_actions_moderator_did_idx": { 1614 + "name": "mod_actions_moderator_did_idx", 1615 + "columns": [ 1616 + { 1617 + "expression": "moderator_did", 1618 + "isExpression": false, 1619 + "asc": true, 1620 + "nulls": "last" 1621 + } 1622 + ], 1623 + "isUnique": false, 1624 + "concurrently": false, 1625 + "method": "btree", 1626 + "with": {} 1627 + }, 1628 + "mod_actions_community_did_idx": { 1629 + "name": "mod_actions_community_did_idx", 1630 + "columns": [ 1631 + { 1632 + "expression": "community_did", 1633 + "isExpression": false, 1634 + "asc": true, 1635 + "nulls": "last" 1636 + } 1637 + ], 1638 + "isUnique": false, 1639 + "concurrently": false, 1640 + "method": "btree", 1641 + "with": {} 1642 + }, 1643 + "mod_actions_created_at_idx": { 1644 + "name": "mod_actions_created_at_idx", 1645 + "columns": [ 1646 + { 1647 + "expression": "created_at", 1648 + "isExpression": false, 1649 + "asc": true, 1650 + "nulls": "last" 1651 + } 1652 + ], 1653 + "isUnique": false, 1654 + "concurrently": false, 1655 + "method": "btree", 1656 + "with": {} 1657 + }, 1658 + "mod_actions_target_uri_idx": { 1659 + "name": "mod_actions_target_uri_idx", 1660 + "columns": [ 1661 + { 1662 + "expression": "target_uri", 1663 + "isExpression": false, 1664 + "asc": true, 1665 + "nulls": "last" 1666 + } 1667 + ], 1668 + "isUnique": false, 1669 + "concurrently": false, 1670 + "method": "btree", 1671 + "with": {} 1672 + }, 1673 + "mod_actions_target_did_idx": { 1674 + "name": "mod_actions_target_did_idx", 1675 + "columns": [ 1676 + { 1677 + "expression": "target_did", 1678 + "isExpression": false, 1679 + "asc": true, 1680 + "nulls": "last" 1681 + } 1682 + ], 1683 + "isUnique": false, 1684 + "concurrently": false, 1685 + "method": "btree", 1686 + "with": {} 1687 + } 1688 + }, 1689 + "foreignKeys": {}, 1690 + "compositePrimaryKeys": {}, 1691 + "uniqueConstraints": {}, 1692 + "policies": { 1693 + "tenant_isolation": { 1694 + "name": "tenant_isolation", 1695 + "as": "PERMISSIVE", 1696 + "for": "ALL", 1697 + "to": [ 1698 + "barazo_app" 1699 + ], 1700 + "using": "community_did = current_setting('app.current_community_did', true)", 1701 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1702 + } 1703 + }, 1704 + "checkConstraints": {}, 1705 + "isRLSEnabled": true 1706 + }, 1707 + "public.reports": { 1708 + "name": "reports", 1709 + "schema": "", 1710 + "columns": { 1711 + "id": { 1712 + "name": "id", 1713 + "type": "serial", 1714 + "primaryKey": true, 1715 + "notNull": true 1716 + }, 1717 + "reporter_did": { 1718 + "name": "reporter_did", 1719 + "type": "text", 1720 + "primaryKey": false, 1721 + "notNull": true 1722 + }, 1723 + "target_uri": { 1724 + "name": "target_uri", 1725 + "type": "text", 1726 + "primaryKey": false, 1727 + "notNull": true 1728 + }, 1729 + "target_did": { 1730 + "name": "target_did", 1731 + "type": "text", 1732 + "primaryKey": false, 1733 + "notNull": true 1734 + }, 1735 + "reason_type": { 1736 + "name": "reason_type", 1737 + "type": "text", 1738 + "primaryKey": false, 1739 + "notNull": true 1740 + }, 1741 + "description": { 1742 + "name": "description", 1743 + "type": "text", 1744 + "primaryKey": false, 1745 + "notNull": false 1746 + }, 1747 + "community_did": { 1748 + "name": "community_did", 1749 + "type": "text", 1750 + "primaryKey": false, 1751 + "notNull": true 1752 + }, 1753 + "status": { 1754 + "name": "status", 1755 + "type": "text", 1756 + "primaryKey": false, 1757 + "notNull": true, 1758 + "default": "'pending'" 1759 + }, 1760 + "resolution_type": { 1761 + "name": "resolution_type", 1762 + "type": "text", 1763 + "primaryKey": false, 1764 + "notNull": false 1765 + }, 1766 + "resolved_by": { 1767 + "name": "resolved_by", 1768 + "type": "text", 1769 + "primaryKey": false, 1770 + "notNull": false 1771 + }, 1772 + "resolved_at": { 1773 + "name": "resolved_at", 1774 + "type": "timestamp with time zone", 1775 + "primaryKey": false, 1776 + "notNull": false 1777 + }, 1778 + "appeal_reason": { 1779 + "name": "appeal_reason", 1780 + "type": "text", 1781 + "primaryKey": false, 1782 + "notNull": false 1783 + }, 1784 + "appealed_at": { 1785 + "name": "appealed_at", 1786 + "type": "timestamp with time zone", 1787 + "primaryKey": false, 1788 + "notNull": false 1789 + }, 1790 + "appeal_status": { 1791 + "name": "appeal_status", 1792 + "type": "text", 1793 + "primaryKey": false, 1794 + "notNull": true, 1795 + "default": "'none'" 1796 + }, 1797 + "created_at": { 1798 + "name": "created_at", 1799 + "type": "timestamp with time zone", 1800 + "primaryKey": false, 1801 + "notNull": true, 1802 + "default": "now()" 1803 + } 1804 + }, 1805 + "indexes": { 1806 + "reports_reporter_did_idx": { 1807 + "name": "reports_reporter_did_idx", 1808 + "columns": [ 1809 + { 1810 + "expression": "reporter_did", 1811 + "isExpression": false, 1812 + "asc": true, 1813 + "nulls": "last" 1814 + } 1815 + ], 1816 + "isUnique": false, 1817 + "concurrently": false, 1818 + "method": "btree", 1819 + "with": {} 1820 + }, 1821 + "reports_target_uri_idx": { 1822 + "name": "reports_target_uri_idx", 1823 + "columns": [ 1824 + { 1825 + "expression": "target_uri", 1826 + "isExpression": false, 1827 + "asc": true, 1828 + "nulls": "last" 1829 + } 1830 + ], 1831 + "isUnique": false, 1832 + "concurrently": false, 1833 + "method": "btree", 1834 + "with": {} 1835 + }, 1836 + "reports_target_did_idx": { 1837 + "name": "reports_target_did_idx", 1838 + "columns": [ 1839 + { 1840 + "expression": "target_did", 1841 + "isExpression": false, 1842 + "asc": true, 1843 + "nulls": "last" 1844 + } 1845 + ], 1846 + "isUnique": false, 1847 + "concurrently": false, 1848 + "method": "btree", 1849 + "with": {} 1850 + }, 1851 + "reports_community_did_idx": { 1852 + "name": "reports_community_did_idx", 1853 + "columns": [ 1854 + { 1855 + "expression": "community_did", 1856 + "isExpression": false, 1857 + "asc": true, 1858 + "nulls": "last" 1859 + } 1860 + ], 1861 + "isUnique": false, 1862 + "concurrently": false, 1863 + "method": "btree", 1864 + "with": {} 1865 + }, 1866 + "reports_status_idx": { 1867 + "name": "reports_status_idx", 1868 + "columns": [ 1869 + { 1870 + "expression": "status", 1871 + "isExpression": false, 1872 + "asc": true, 1873 + "nulls": "last" 1874 + } 1875 + ], 1876 + "isUnique": false, 1877 + "concurrently": false, 1878 + "method": "btree", 1879 + "with": {} 1880 + }, 1881 + "reports_created_at_idx": { 1882 + "name": "reports_created_at_idx", 1883 + "columns": [ 1884 + { 1885 + "expression": "created_at", 1886 + "isExpression": false, 1887 + "asc": true, 1888 + "nulls": "last" 1889 + } 1890 + ], 1891 + "isUnique": false, 1892 + "concurrently": false, 1893 + "method": "btree", 1894 + "with": {} 1895 + }, 1896 + "reports_unique_reporter_target_idx": { 1897 + "name": "reports_unique_reporter_target_idx", 1898 + "columns": [ 1899 + { 1900 + "expression": "reporter_did", 1901 + "isExpression": false, 1902 + "asc": true, 1903 + "nulls": "last" 1904 + }, 1905 + { 1906 + "expression": "target_uri", 1907 + "isExpression": false, 1908 + "asc": true, 1909 + "nulls": "last" 1910 + }, 1911 + { 1912 + "expression": "community_did", 1913 + "isExpression": false, 1914 + "asc": true, 1915 + "nulls": "last" 1916 + } 1917 + ], 1918 + "isUnique": true, 1919 + "concurrently": false, 1920 + "method": "btree", 1921 + "with": {} 1922 + } 1923 + }, 1924 + "foreignKeys": {}, 1925 + "compositePrimaryKeys": {}, 1926 + "uniqueConstraints": {}, 1927 + "policies": { 1928 + "tenant_isolation": { 1929 + "name": "tenant_isolation", 1930 + "as": "PERMISSIVE", 1931 + "for": "ALL", 1932 + "to": [ 1933 + "barazo_app" 1934 + ], 1935 + "using": "community_did = current_setting('app.current_community_did', true)", 1936 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 1937 + } 1938 + }, 1939 + "checkConstraints": {}, 1940 + "isRLSEnabled": true 1941 + }, 1942 + "public.notifications": { 1943 + "name": "notifications", 1944 + "schema": "", 1945 + "columns": { 1946 + "id": { 1947 + "name": "id", 1948 + "type": "serial", 1949 + "primaryKey": true, 1950 + "notNull": true 1951 + }, 1952 + "recipient_did": { 1953 + "name": "recipient_did", 1954 + "type": "text", 1955 + "primaryKey": false, 1956 + "notNull": true 1957 + }, 1958 + "type": { 1959 + "name": "type", 1960 + "type": "text", 1961 + "primaryKey": false, 1962 + "notNull": true 1963 + }, 1964 + "subject_uri": { 1965 + "name": "subject_uri", 1966 + "type": "text", 1967 + "primaryKey": false, 1968 + "notNull": true 1969 + }, 1970 + "actor_did": { 1971 + "name": "actor_did", 1972 + "type": "text", 1973 + "primaryKey": false, 1974 + "notNull": true 1975 + }, 1976 + "community_did": { 1977 + "name": "community_did", 1978 + "type": "text", 1979 + "primaryKey": false, 1980 + "notNull": true 1981 + }, 1982 + "read": { 1983 + "name": "read", 1984 + "type": "boolean", 1985 + "primaryKey": false, 1986 + "notNull": true, 1987 + "default": false 1988 + }, 1989 + "created_at": { 1990 + "name": "created_at", 1991 + "type": "timestamp with time zone", 1992 + "primaryKey": false, 1993 + "notNull": true, 1994 + "default": "now()" 1995 + } 1996 + }, 1997 + "indexes": { 1998 + "notifications_recipient_did_idx": { 1999 + "name": "notifications_recipient_did_idx", 2000 + "columns": [ 2001 + { 2002 + "expression": "recipient_did", 2003 + "isExpression": false, 2004 + "asc": true, 2005 + "nulls": "last" 2006 + } 2007 + ], 2008 + "isUnique": false, 2009 + "concurrently": false, 2010 + "method": "btree", 2011 + "with": {} 2012 + }, 2013 + "notifications_recipient_read_idx": { 2014 + "name": "notifications_recipient_read_idx", 2015 + "columns": [ 2016 + { 2017 + "expression": "recipient_did", 2018 + "isExpression": false, 2019 + "asc": true, 2020 + "nulls": "last" 2021 + }, 2022 + { 2023 + "expression": "read", 2024 + "isExpression": false, 2025 + "asc": true, 2026 + "nulls": "last" 2027 + } 2028 + ], 2029 + "isUnique": false, 2030 + "concurrently": false, 2031 + "method": "btree", 2032 + "with": {} 2033 + }, 2034 + "notifications_created_at_idx": { 2035 + "name": "notifications_created_at_idx", 2036 + "columns": [ 2037 + { 2038 + "expression": "created_at", 2039 + "isExpression": false, 2040 + "asc": true, 2041 + "nulls": "last" 2042 + } 2043 + ], 2044 + "isUnique": false, 2045 + "concurrently": false, 2046 + "method": "btree", 2047 + "with": {} 2048 + } 2049 + }, 2050 + "foreignKeys": {}, 2051 + "compositePrimaryKeys": {}, 2052 + "uniqueConstraints": {}, 2053 + "policies": { 2054 + "tenant_isolation": { 2055 + "name": "tenant_isolation", 2056 + "as": "PERMISSIVE", 2057 + "for": "ALL", 2058 + "to": [ 2059 + "barazo_app" 2060 + ], 2061 + "using": "community_did = current_setting('app.current_community_did', true)", 2062 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2063 + } 2064 + }, 2065 + "checkConstraints": {}, 2066 + "isRLSEnabled": true 2067 + }, 2068 + "public.user_community_preferences": { 2069 + "name": "user_community_preferences", 2070 + "schema": "", 2071 + "columns": { 2072 + "did": { 2073 + "name": "did", 2074 + "type": "text", 2075 + "primaryKey": false, 2076 + "notNull": true 2077 + }, 2078 + "community_did": { 2079 + "name": "community_did", 2080 + "type": "text", 2081 + "primaryKey": false, 2082 + "notNull": true 2083 + }, 2084 + "maturity_override": { 2085 + "name": "maturity_override", 2086 + "type": "text", 2087 + "primaryKey": false, 2088 + "notNull": false 2089 + }, 2090 + "muted_words": { 2091 + "name": "muted_words", 2092 + "type": "jsonb", 2093 + "primaryKey": false, 2094 + "notNull": false 2095 + }, 2096 + "blocked_dids": { 2097 + "name": "blocked_dids", 2098 + "type": "jsonb", 2099 + "primaryKey": false, 2100 + "notNull": false 2101 + }, 2102 + "muted_dids": { 2103 + "name": "muted_dids", 2104 + "type": "jsonb", 2105 + "primaryKey": false, 2106 + "notNull": false 2107 + }, 2108 + "notification_prefs": { 2109 + "name": "notification_prefs", 2110 + "type": "jsonb", 2111 + "primaryKey": false, 2112 + "notNull": false 2113 + }, 2114 + "updated_at": { 2115 + "name": "updated_at", 2116 + "type": "timestamp with time zone", 2117 + "primaryKey": false, 2118 + "notNull": true, 2119 + "default": "now()" 2120 + } 2121 + }, 2122 + "indexes": { 2123 + "user_community_prefs_did_idx": { 2124 + "name": "user_community_prefs_did_idx", 2125 + "columns": [ 2126 + { 2127 + "expression": "did", 2128 + "isExpression": false, 2129 + "asc": true, 2130 + "nulls": "last" 2131 + } 2132 + ], 2133 + "isUnique": false, 2134 + "concurrently": false, 2135 + "method": "btree", 2136 + "with": {} 2137 + }, 2138 + "user_community_prefs_community_idx": { 2139 + "name": "user_community_prefs_community_idx", 2140 + "columns": [ 2141 + { 2142 + "expression": "community_did", 2143 + "isExpression": false, 2144 + "asc": true, 2145 + "nulls": "last" 2146 + } 2147 + ], 2148 + "isUnique": false, 2149 + "concurrently": false, 2150 + "method": "btree", 2151 + "with": {} 2152 + } 2153 + }, 2154 + "foreignKeys": {}, 2155 + "compositePrimaryKeys": { 2156 + "user_community_preferences_did_community_did_pk": { 2157 + "name": "user_community_preferences_did_community_did_pk", 2158 + "columns": [ 2159 + "did", 2160 + "community_did" 2161 + ] 2162 + } 2163 + }, 2164 + "uniqueConstraints": {}, 2165 + "policies": { 2166 + "tenant_isolation": { 2167 + "name": "tenant_isolation", 2168 + "as": "PERMISSIVE", 2169 + "for": "ALL", 2170 + "to": [ 2171 + "barazo_app" 2172 + ], 2173 + "using": "community_did = current_setting('app.current_community_did', true)", 2174 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2175 + } 2176 + }, 2177 + "checkConstraints": {}, 2178 + "isRLSEnabled": true 2179 + }, 2180 + "public.user_preferences": { 2181 + "name": "user_preferences", 2182 + "schema": "", 2183 + "columns": { 2184 + "did": { 2185 + "name": "did", 2186 + "type": "text", 2187 + "primaryKey": true, 2188 + "notNull": true 2189 + }, 2190 + "maturity_level": { 2191 + "name": "maturity_level", 2192 + "type": "text", 2193 + "primaryKey": false, 2194 + "notNull": true, 2195 + "default": "'sfw'" 2196 + }, 2197 + "declared_age": { 2198 + "name": "declared_age", 2199 + "type": "integer", 2200 + "primaryKey": false, 2201 + "notNull": false 2202 + }, 2203 + "muted_words": { 2204 + "name": "muted_words", 2205 + "type": "jsonb", 2206 + "primaryKey": false, 2207 + "notNull": true, 2208 + "default": "'[]'::jsonb" 2209 + }, 2210 + "blocked_dids": { 2211 + "name": "blocked_dids", 2212 + "type": "jsonb", 2213 + "primaryKey": false, 2214 + "notNull": true, 2215 + "default": "'[]'::jsonb" 2216 + }, 2217 + "muted_dids": { 2218 + "name": "muted_dids", 2219 + "type": "jsonb", 2220 + "primaryKey": false, 2221 + "notNull": true, 2222 + "default": "'[]'::jsonb" 2223 + }, 2224 + "cross_post_bluesky": { 2225 + "name": "cross_post_bluesky", 2226 + "type": "boolean", 2227 + "primaryKey": false, 2228 + "notNull": true, 2229 + "default": false 2230 + }, 2231 + "cross_post_frontpage": { 2232 + "name": "cross_post_frontpage", 2233 + "type": "boolean", 2234 + "primaryKey": false, 2235 + "notNull": true, 2236 + "default": false 2237 + }, 2238 + "cross_post_scopes_granted": { 2239 + "name": "cross_post_scopes_granted", 2240 + "type": "boolean", 2241 + "primaryKey": false, 2242 + "notNull": true, 2243 + "default": false 2244 + }, 2245 + "updated_at": { 2246 + "name": "updated_at", 2247 + "type": "timestamp with time zone", 2248 + "primaryKey": false, 2249 + "notNull": true, 2250 + "default": "now()" 2251 + } 2252 + }, 2253 + "indexes": {}, 2254 + "foreignKeys": {}, 2255 + "compositePrimaryKeys": {}, 2256 + "uniqueConstraints": {}, 2257 + "policies": {}, 2258 + "checkConstraints": {}, 2259 + "isRLSEnabled": false 2260 + }, 2261 + "public.cross_posts": { 2262 + "name": "cross_posts", 2263 + "schema": "", 2264 + "columns": { 2265 + "id": { 2266 + "name": "id", 2267 + "type": "text", 2268 + "primaryKey": true, 2269 + "notNull": true 2270 + }, 2271 + "topic_uri": { 2272 + "name": "topic_uri", 2273 + "type": "text", 2274 + "primaryKey": false, 2275 + "notNull": true 2276 + }, 2277 + "service": { 2278 + "name": "service", 2279 + "type": "text", 2280 + "primaryKey": false, 2281 + "notNull": true 2282 + }, 2283 + "cross_post_uri": { 2284 + "name": "cross_post_uri", 2285 + "type": "text", 2286 + "primaryKey": false, 2287 + "notNull": true 2288 + }, 2289 + "cross_post_cid": { 2290 + "name": "cross_post_cid", 2291 + "type": "text", 2292 + "primaryKey": false, 2293 + "notNull": true 2294 + }, 2295 + "author_did": { 2296 + "name": "author_did", 2297 + "type": "text", 2298 + "primaryKey": false, 2299 + "notNull": true 2300 + }, 2301 + "created_at": { 2302 + "name": "created_at", 2303 + "type": "timestamp with time zone", 2304 + "primaryKey": false, 2305 + "notNull": true, 2306 + "default": "now()" 2307 + } 2308 + }, 2309 + "indexes": { 2310 + "cross_posts_topic_uri_idx": { 2311 + "name": "cross_posts_topic_uri_idx", 2312 + "columns": [ 2313 + { 2314 + "expression": "topic_uri", 2315 + "isExpression": false, 2316 + "asc": true, 2317 + "nulls": "last" 2318 + } 2319 + ], 2320 + "isUnique": false, 2321 + "concurrently": false, 2322 + "method": "btree", 2323 + "with": {} 2324 + }, 2325 + "cross_posts_author_did_idx": { 2326 + "name": "cross_posts_author_did_idx", 2327 + "columns": [ 2328 + { 2329 + "expression": "author_did", 2330 + "isExpression": false, 2331 + "asc": true, 2332 + "nulls": "last" 2333 + } 2334 + ], 2335 + "isUnique": false, 2336 + "concurrently": false, 2337 + "method": "btree", 2338 + "with": {} 2339 + } 2340 + }, 2341 + "foreignKeys": {}, 2342 + "compositePrimaryKeys": {}, 2343 + "uniqueConstraints": {}, 2344 + "policies": {}, 2345 + "checkConstraints": {}, 2346 + "isRLSEnabled": false 2347 + }, 2348 + "public.community_onboarding_fields": { 2349 + "name": "community_onboarding_fields", 2350 + "schema": "", 2351 + "columns": { 2352 + "id": { 2353 + "name": "id", 2354 + "type": "text", 2355 + "primaryKey": true, 2356 + "notNull": true 2357 + }, 2358 + "community_did": { 2359 + "name": "community_did", 2360 + "type": "text", 2361 + "primaryKey": false, 2362 + "notNull": true 2363 + }, 2364 + "field_type": { 2365 + "name": "field_type", 2366 + "type": "text", 2367 + "primaryKey": false, 2368 + "notNull": true 2369 + }, 2370 + "label": { 2371 + "name": "label", 2372 + "type": "text", 2373 + "primaryKey": false, 2374 + "notNull": true 2375 + }, 2376 + "description": { 2377 + "name": "description", 2378 + "type": "text", 2379 + "primaryKey": false, 2380 + "notNull": false 2381 + }, 2382 + "is_mandatory": { 2383 + "name": "is_mandatory", 2384 + "type": "boolean", 2385 + "primaryKey": false, 2386 + "notNull": true, 2387 + "default": true 2388 + }, 2389 + "sort_order": { 2390 + "name": "sort_order", 2391 + "type": "integer", 2392 + "primaryKey": false, 2393 + "notNull": true, 2394 + "default": 0 2395 + }, 2396 + "source": { 2397 + "name": "source", 2398 + "type": "text", 2399 + "primaryKey": false, 2400 + "notNull": true, 2401 + "default": "'admin'" 2402 + }, 2403 + "config": { 2404 + "name": "config", 2405 + "type": "jsonb", 2406 + "primaryKey": false, 2407 + "notNull": false 2408 + }, 2409 + "created_at": { 2410 + "name": "created_at", 2411 + "type": "timestamp with time zone", 2412 + "primaryKey": false, 2413 + "notNull": true, 2414 + "default": "now()" 2415 + }, 2416 + "updated_at": { 2417 + "name": "updated_at", 2418 + "type": "timestamp with time zone", 2419 + "primaryKey": false, 2420 + "notNull": true, 2421 + "default": "now()" 2422 + } 2423 + }, 2424 + "indexes": { 2425 + "onboarding_fields_community_idx": { 2426 + "name": "onboarding_fields_community_idx", 2427 + "columns": [ 2428 + { 2429 + "expression": "community_did", 2430 + "isExpression": false, 2431 + "asc": true, 2432 + "nulls": "last" 2433 + } 2434 + ], 2435 + "isUnique": false, 2436 + "concurrently": false, 2437 + "method": "btree", 2438 + "with": {} 2439 + } 2440 + }, 2441 + "foreignKeys": {}, 2442 + "compositePrimaryKeys": {}, 2443 + "uniqueConstraints": {}, 2444 + "policies": { 2445 + "tenant_isolation": { 2446 + "name": "tenant_isolation", 2447 + "as": "PERMISSIVE", 2448 + "for": "ALL", 2449 + "to": [ 2450 + "barazo_app" 2451 + ], 2452 + "using": "community_did = current_setting('app.current_community_did', true)", 2453 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2454 + } 2455 + }, 2456 + "checkConstraints": {}, 2457 + "isRLSEnabled": true 2458 + }, 2459 + "public.user_onboarding_responses": { 2460 + "name": "user_onboarding_responses", 2461 + "schema": "", 2462 + "columns": { 2463 + "did": { 2464 + "name": "did", 2465 + "type": "text", 2466 + "primaryKey": false, 2467 + "notNull": true 2468 + }, 2469 + "community_did": { 2470 + "name": "community_did", 2471 + "type": "text", 2472 + "primaryKey": false, 2473 + "notNull": true 2474 + }, 2475 + "field_id": { 2476 + "name": "field_id", 2477 + "type": "text", 2478 + "primaryKey": false, 2479 + "notNull": true 2480 + }, 2481 + "response": { 2482 + "name": "response", 2483 + "type": "jsonb", 2484 + "primaryKey": false, 2485 + "notNull": true 2486 + }, 2487 + "completed_at": { 2488 + "name": "completed_at", 2489 + "type": "timestamp with time zone", 2490 + "primaryKey": false, 2491 + "notNull": true, 2492 + "default": "now()" 2493 + } 2494 + }, 2495 + "indexes": { 2496 + "onboarding_responses_did_community_idx": { 2497 + "name": "onboarding_responses_did_community_idx", 2498 + "columns": [ 2499 + { 2500 + "expression": "did", 2501 + "isExpression": false, 2502 + "asc": true, 2503 + "nulls": "last" 2504 + }, 2505 + { 2506 + "expression": "community_did", 2507 + "isExpression": false, 2508 + "asc": true, 2509 + "nulls": "last" 2510 + } 2511 + ], 2512 + "isUnique": false, 2513 + "concurrently": false, 2514 + "method": "btree", 2515 + "with": {} 2516 + } 2517 + }, 2518 + "foreignKeys": {}, 2519 + "compositePrimaryKeys": { 2520 + "user_onboarding_responses_did_community_did_field_id_pk": { 2521 + "name": "user_onboarding_responses_did_community_did_field_id_pk", 2522 + "columns": [ 2523 + "did", 2524 + "community_did", 2525 + "field_id" 2526 + ] 2527 + } 2528 + }, 2529 + "uniqueConstraints": {}, 2530 + "policies": { 2531 + "tenant_isolation": { 2532 + "name": "tenant_isolation", 2533 + "as": "PERMISSIVE", 2534 + "for": "ALL", 2535 + "to": [ 2536 + "barazo_app" 2537 + ], 2538 + "using": "community_did = current_setting('app.current_community_did', true)", 2539 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2540 + } 2541 + }, 2542 + "checkConstraints": {}, 2543 + "isRLSEnabled": true 2544 + }, 2545 + "public.moderation_queue": { 2546 + "name": "moderation_queue", 2547 + "schema": "", 2548 + "columns": { 2549 + "id": { 2550 + "name": "id", 2551 + "type": "serial", 2552 + "primaryKey": true, 2553 + "notNull": true 2554 + }, 2555 + "content_uri": { 2556 + "name": "content_uri", 2557 + "type": "text", 2558 + "primaryKey": false, 2559 + "notNull": true 2560 + }, 2561 + "content_type": { 2562 + "name": "content_type", 2563 + "type": "text", 2564 + "primaryKey": false, 2565 + "notNull": true 2566 + }, 2567 + "author_did": { 2568 + "name": "author_did", 2569 + "type": "text", 2570 + "primaryKey": false, 2571 + "notNull": true 2572 + }, 2573 + "community_did": { 2574 + "name": "community_did", 2575 + "type": "text", 2576 + "primaryKey": false, 2577 + "notNull": true 2578 + }, 2579 + "queue_reason": { 2580 + "name": "queue_reason", 2581 + "type": "text", 2582 + "primaryKey": false, 2583 + "notNull": true 2584 + }, 2585 + "matched_words": { 2586 + "name": "matched_words", 2587 + "type": "jsonb", 2588 + "primaryKey": false, 2589 + "notNull": false 2590 + }, 2591 + "status": { 2592 + "name": "status", 2593 + "type": "text", 2594 + "primaryKey": false, 2595 + "notNull": true, 2596 + "default": "'pending'" 2597 + }, 2598 + "reviewed_by": { 2599 + "name": "reviewed_by", 2600 + "type": "text", 2601 + "primaryKey": false, 2602 + "notNull": false 2603 + }, 2604 + "created_at": { 2605 + "name": "created_at", 2606 + "type": "timestamp with time zone", 2607 + "primaryKey": false, 2608 + "notNull": true, 2609 + "default": "now()" 2610 + }, 2611 + "reviewed_at": { 2612 + "name": "reviewed_at", 2613 + "type": "timestamp with time zone", 2614 + "primaryKey": false, 2615 + "notNull": false 2616 + } 2617 + }, 2618 + "indexes": { 2619 + "mod_queue_author_did_idx": { 2620 + "name": "mod_queue_author_did_idx", 2621 + "columns": [ 2622 + { 2623 + "expression": "author_did", 2624 + "isExpression": false, 2625 + "asc": true, 2626 + "nulls": "last" 2627 + } 2628 + ], 2629 + "isUnique": false, 2630 + "concurrently": false, 2631 + "method": "btree", 2632 + "with": {} 2633 + }, 2634 + "mod_queue_community_did_idx": { 2635 + "name": "mod_queue_community_did_idx", 2636 + "columns": [ 2637 + { 2638 + "expression": "community_did", 2639 + "isExpression": false, 2640 + "asc": true, 2641 + "nulls": "last" 2642 + } 2643 + ], 2644 + "isUnique": false, 2645 + "concurrently": false, 2646 + "method": "btree", 2647 + "with": {} 2648 + }, 2649 + "mod_queue_status_idx": { 2650 + "name": "mod_queue_status_idx", 2651 + "columns": [ 2652 + { 2653 + "expression": "status", 2654 + "isExpression": false, 2655 + "asc": true, 2656 + "nulls": "last" 2657 + } 2658 + ], 2659 + "isUnique": false, 2660 + "concurrently": false, 2661 + "method": "btree", 2662 + "with": {} 2663 + }, 2664 + "mod_queue_created_at_idx": { 2665 + "name": "mod_queue_created_at_idx", 2666 + "columns": [ 2667 + { 2668 + "expression": "created_at", 2669 + "isExpression": false, 2670 + "asc": true, 2671 + "nulls": "last" 2672 + } 2673 + ], 2674 + "isUnique": false, 2675 + "concurrently": false, 2676 + "method": "btree", 2677 + "with": {} 2678 + }, 2679 + "mod_queue_content_uri_idx": { 2680 + "name": "mod_queue_content_uri_idx", 2681 + "columns": [ 2682 + { 2683 + "expression": "content_uri", 2684 + "isExpression": false, 2685 + "asc": true, 2686 + "nulls": "last" 2687 + } 2688 + ], 2689 + "isUnique": false, 2690 + "concurrently": false, 2691 + "method": "btree", 2692 + "with": {} 2693 + } 2694 + }, 2695 + "foreignKeys": {}, 2696 + "compositePrimaryKeys": {}, 2697 + "uniqueConstraints": {}, 2698 + "policies": { 2699 + "tenant_isolation": { 2700 + "name": "tenant_isolation", 2701 + "as": "PERMISSIVE", 2702 + "for": "ALL", 2703 + "to": [ 2704 + "barazo_app" 2705 + ], 2706 + "using": "community_did = current_setting('app.current_community_did', true)", 2707 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2708 + } 2709 + }, 2710 + "checkConstraints": {}, 2711 + "isRLSEnabled": true 2712 + }, 2713 + "public.account_trust": { 2714 + "name": "account_trust", 2715 + "schema": "", 2716 + "columns": { 2717 + "id": { 2718 + "name": "id", 2719 + "type": "serial", 2720 + "primaryKey": true, 2721 + "notNull": true 2722 + }, 2723 + "did": { 2724 + "name": "did", 2725 + "type": "text", 2726 + "primaryKey": false, 2727 + "notNull": true 2728 + }, 2729 + "community_did": { 2730 + "name": "community_did", 2731 + "type": "text", 2732 + "primaryKey": false, 2733 + "notNull": true 2734 + }, 2735 + "approved_post_count": { 2736 + "name": "approved_post_count", 2737 + "type": "integer", 2738 + "primaryKey": false, 2739 + "notNull": true, 2740 + "default": 0 2741 + }, 2742 + "is_trusted": { 2743 + "name": "is_trusted", 2744 + "type": "boolean", 2745 + "primaryKey": false, 2746 + "notNull": true, 2747 + "default": false 2748 + }, 2749 + "trusted_at": { 2750 + "name": "trusted_at", 2751 + "type": "timestamp with time zone", 2752 + "primaryKey": false, 2753 + "notNull": false 2754 + } 2755 + }, 2756 + "indexes": { 2757 + "account_trust_did_community_idx": { 2758 + "name": "account_trust_did_community_idx", 2759 + "columns": [ 2760 + { 2761 + "expression": "did", 2762 + "isExpression": false, 2763 + "asc": true, 2764 + "nulls": "last" 2765 + }, 2766 + { 2767 + "expression": "community_did", 2768 + "isExpression": false, 2769 + "asc": true, 2770 + "nulls": "last" 2771 + } 2772 + ], 2773 + "isUnique": true, 2774 + "concurrently": false, 2775 + "method": "btree", 2776 + "with": {} 2777 + }, 2778 + "account_trust_did_idx": { 2779 + "name": "account_trust_did_idx", 2780 + "columns": [ 2781 + { 2782 + "expression": "did", 2783 + "isExpression": false, 2784 + "asc": true, 2785 + "nulls": "last" 2786 + } 2787 + ], 2788 + "isUnique": false, 2789 + "concurrently": false, 2790 + "method": "btree", 2791 + "with": {} 2792 + } 2793 + }, 2794 + "foreignKeys": {}, 2795 + "compositePrimaryKeys": {}, 2796 + "uniqueConstraints": {}, 2797 + "policies": { 2798 + "tenant_isolation": { 2799 + "name": "tenant_isolation", 2800 + "as": "PERMISSIVE", 2801 + "for": "ALL", 2802 + "to": [ 2803 + "barazo_app" 2804 + ], 2805 + "using": "community_did = current_setting('app.current_community_did', true)", 2806 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2807 + } 2808 + }, 2809 + "checkConstraints": {}, 2810 + "isRLSEnabled": true 2811 + }, 2812 + "public.community_filters": { 2813 + "name": "community_filters", 2814 + "schema": "", 2815 + "columns": { 2816 + "community_did": { 2817 + "name": "community_did", 2818 + "type": "text", 2819 + "primaryKey": true, 2820 + "notNull": true 2821 + }, 2822 + "status": { 2823 + "name": "status", 2824 + "type": "text", 2825 + "primaryKey": false, 2826 + "notNull": true, 2827 + "default": "'active'" 2828 + }, 2829 + "admin_did": { 2830 + "name": "admin_did", 2831 + "type": "text", 2832 + "primaryKey": false, 2833 + "notNull": false 2834 + }, 2835 + "reason": { 2836 + "name": "reason", 2837 + "type": "text", 2838 + "primaryKey": false, 2839 + "notNull": false 2840 + }, 2841 + "report_count": { 2842 + "name": "report_count", 2843 + "type": "integer", 2844 + "primaryKey": false, 2845 + "notNull": true, 2846 + "default": 0 2847 + }, 2848 + "last_reviewed_at": { 2849 + "name": "last_reviewed_at", 2850 + "type": "timestamp with time zone", 2851 + "primaryKey": false, 2852 + "notNull": false 2853 + }, 2854 + "filtered_by": { 2855 + "name": "filtered_by", 2856 + "type": "text", 2857 + "primaryKey": false, 2858 + "notNull": false 2859 + }, 2860 + "created_at": { 2861 + "name": "created_at", 2862 + "type": "timestamp with time zone", 2863 + "primaryKey": false, 2864 + "notNull": true, 2865 + "default": "now()" 2866 + }, 2867 + "updated_at": { 2868 + "name": "updated_at", 2869 + "type": "timestamp with time zone", 2870 + "primaryKey": false, 2871 + "notNull": true, 2872 + "default": "now()" 2873 + } 2874 + }, 2875 + "indexes": { 2876 + "community_filters_status_idx": { 2877 + "name": "community_filters_status_idx", 2878 + "columns": [ 2879 + { 2880 + "expression": "status", 2881 + "isExpression": false, 2882 + "asc": true, 2883 + "nulls": "last" 2884 + } 2885 + ], 2886 + "isUnique": false, 2887 + "concurrently": false, 2888 + "method": "btree", 2889 + "with": {} 2890 + }, 2891 + "community_filters_admin_did_idx": { 2892 + "name": "community_filters_admin_did_idx", 2893 + "columns": [ 2894 + { 2895 + "expression": "admin_did", 2896 + "isExpression": false, 2897 + "asc": true, 2898 + "nulls": "last" 2899 + } 2900 + ], 2901 + "isUnique": false, 2902 + "concurrently": false, 2903 + "method": "btree", 2904 + "with": {} 2905 + }, 2906 + "community_filters_updated_at_idx": { 2907 + "name": "community_filters_updated_at_idx", 2908 + "columns": [ 2909 + { 2910 + "expression": "updated_at", 2911 + "isExpression": false, 2912 + "asc": true, 2913 + "nulls": "last" 2914 + } 2915 + ], 2916 + "isUnique": false, 2917 + "concurrently": false, 2918 + "method": "btree", 2919 + "with": {} 2920 + } 2921 + }, 2922 + "foreignKeys": {}, 2923 + "compositePrimaryKeys": {}, 2924 + "uniqueConstraints": {}, 2925 + "policies": { 2926 + "tenant_isolation": { 2927 + "name": "tenant_isolation", 2928 + "as": "PERMISSIVE", 2929 + "for": "ALL", 2930 + "to": [ 2931 + "barazo_app" 2932 + ], 2933 + "using": "community_did = current_setting('app.current_community_did', true)", 2934 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 2935 + } 2936 + }, 2937 + "checkConstraints": {}, 2938 + "isRLSEnabled": true 2939 + }, 2940 + "public.account_filters": { 2941 + "name": "account_filters", 2942 + "schema": "", 2943 + "columns": { 2944 + "id": { 2945 + "name": "id", 2946 + "type": "serial", 2947 + "primaryKey": true, 2948 + "notNull": true 2949 + }, 2950 + "did": { 2951 + "name": "did", 2952 + "type": "text", 2953 + "primaryKey": false, 2954 + "notNull": true 2955 + }, 2956 + "community_did": { 2957 + "name": "community_did", 2958 + "type": "text", 2959 + "primaryKey": false, 2960 + "notNull": true 2961 + }, 2962 + "status": { 2963 + "name": "status", 2964 + "type": "text", 2965 + "primaryKey": false, 2966 + "notNull": true, 2967 + "default": "'active'" 2968 + }, 2969 + "reason": { 2970 + "name": "reason", 2971 + "type": "text", 2972 + "primaryKey": false, 2973 + "notNull": false 2974 + }, 2975 + "report_count": { 2976 + "name": "report_count", 2977 + "type": "integer", 2978 + "primaryKey": false, 2979 + "notNull": true, 2980 + "default": 0 2981 + }, 2982 + "ban_count": { 2983 + "name": "ban_count", 2984 + "type": "integer", 2985 + "primaryKey": false, 2986 + "notNull": true, 2987 + "default": 0 2988 + }, 2989 + "last_reviewed_at": { 2990 + "name": "last_reviewed_at", 2991 + "type": "timestamp with time zone", 2992 + "primaryKey": false, 2993 + "notNull": false 2994 + }, 2995 + "filtered_by": { 2996 + "name": "filtered_by", 2997 + "type": "text", 2998 + "primaryKey": false, 2999 + "notNull": false 3000 + }, 3001 + "created_at": { 3002 + "name": "created_at", 3003 + "type": "timestamp with time zone", 3004 + "primaryKey": false, 3005 + "notNull": true, 3006 + "default": "now()" 3007 + }, 3008 + "updated_at": { 3009 + "name": "updated_at", 3010 + "type": "timestamp with time zone", 3011 + "primaryKey": false, 3012 + "notNull": true, 3013 + "default": "now()" 3014 + } 3015 + }, 3016 + "indexes": { 3017 + "account_filters_did_community_idx": { 3018 + "name": "account_filters_did_community_idx", 3019 + "columns": [ 3020 + { 3021 + "expression": "did", 3022 + "isExpression": false, 3023 + "asc": true, 3024 + "nulls": "last" 3025 + }, 3026 + { 3027 + "expression": "community_did", 3028 + "isExpression": false, 3029 + "asc": true, 3030 + "nulls": "last" 3031 + } 3032 + ], 3033 + "isUnique": true, 3034 + "concurrently": false, 3035 + "method": "btree", 3036 + "with": {} 3037 + }, 3038 + "account_filters_did_idx": { 3039 + "name": "account_filters_did_idx", 3040 + "columns": [ 3041 + { 3042 + "expression": "did", 3043 + "isExpression": false, 3044 + "asc": true, 3045 + "nulls": "last" 3046 + } 3047 + ], 3048 + "isUnique": false, 3049 + "concurrently": false, 3050 + "method": "btree", 3051 + "with": {} 3052 + }, 3053 + "account_filters_community_did_idx": { 3054 + "name": "account_filters_community_did_idx", 3055 + "columns": [ 3056 + { 3057 + "expression": "community_did", 3058 + "isExpression": false, 3059 + "asc": true, 3060 + "nulls": "last" 3061 + } 3062 + ], 3063 + "isUnique": false, 3064 + "concurrently": false, 3065 + "method": "btree", 3066 + "with": {} 3067 + }, 3068 + "account_filters_status_idx": { 3069 + "name": "account_filters_status_idx", 3070 + "columns": [ 3071 + { 3072 + "expression": "status", 3073 + "isExpression": false, 3074 + "asc": true, 3075 + "nulls": "last" 3076 + } 3077 + ], 3078 + "isUnique": false, 3079 + "concurrently": false, 3080 + "method": "btree", 3081 + "with": {} 3082 + }, 3083 + "account_filters_updated_at_idx": { 3084 + "name": "account_filters_updated_at_idx", 3085 + "columns": [ 3086 + { 3087 + "expression": "updated_at", 3088 + "isExpression": false, 3089 + "asc": true, 3090 + "nulls": "last" 3091 + } 3092 + ], 3093 + "isUnique": false, 3094 + "concurrently": false, 3095 + "method": "btree", 3096 + "with": {} 3097 + } 3098 + }, 3099 + "foreignKeys": {}, 3100 + "compositePrimaryKeys": {}, 3101 + "uniqueConstraints": {}, 3102 + "policies": { 3103 + "tenant_isolation": { 3104 + "name": "tenant_isolation", 3105 + "as": "PERMISSIVE", 3106 + "for": "ALL", 3107 + "to": [ 3108 + "barazo_app" 3109 + ], 3110 + "using": "community_did = current_setting('app.current_community_did', true)", 3111 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 3112 + } 3113 + }, 3114 + "checkConstraints": {}, 3115 + "isRLSEnabled": true 3116 + }, 3117 + "public.ozone_labels": { 3118 + "name": "ozone_labels", 3119 + "schema": "", 3120 + "columns": { 3121 + "id": { 3122 + "name": "id", 3123 + "type": "serial", 3124 + "primaryKey": true, 3125 + "notNull": true 3126 + }, 3127 + "src": { 3128 + "name": "src", 3129 + "type": "text", 3130 + "primaryKey": false, 3131 + "notNull": true 3132 + }, 3133 + "uri": { 3134 + "name": "uri", 3135 + "type": "text", 3136 + "primaryKey": false, 3137 + "notNull": true 3138 + }, 3139 + "val": { 3140 + "name": "val", 3141 + "type": "text", 3142 + "primaryKey": false, 3143 + "notNull": true 3144 + }, 3145 + "neg": { 3146 + "name": "neg", 3147 + "type": "boolean", 3148 + "primaryKey": false, 3149 + "notNull": true, 3150 + "default": false 3151 + }, 3152 + "cts": { 3153 + "name": "cts", 3154 + "type": "timestamp with time zone", 3155 + "primaryKey": false, 3156 + "notNull": true 3157 + }, 3158 + "exp": { 3159 + "name": "exp", 3160 + "type": "timestamp with time zone", 3161 + "primaryKey": false, 3162 + "notNull": false 3163 + }, 3164 + "indexed_at": { 3165 + "name": "indexed_at", 3166 + "type": "timestamp with time zone", 3167 + "primaryKey": false, 3168 + "notNull": true, 3169 + "default": "now()" 3170 + } 3171 + }, 3172 + "indexes": { 3173 + "ozone_labels_src_uri_val_idx": { 3174 + "name": "ozone_labels_src_uri_val_idx", 3175 + "columns": [ 3176 + { 3177 + "expression": "src", 3178 + "isExpression": false, 3179 + "asc": true, 3180 + "nulls": "last" 3181 + }, 3182 + { 3183 + "expression": "uri", 3184 + "isExpression": false, 3185 + "asc": true, 3186 + "nulls": "last" 3187 + }, 3188 + { 3189 + "expression": "val", 3190 + "isExpression": false, 3191 + "asc": true, 3192 + "nulls": "last" 3193 + } 3194 + ], 3195 + "isUnique": true, 3196 + "concurrently": false, 3197 + "method": "btree", 3198 + "with": {} 3199 + }, 3200 + "ozone_labels_uri_idx": { 3201 + "name": "ozone_labels_uri_idx", 3202 + "columns": [ 3203 + { 3204 + "expression": "uri", 3205 + "isExpression": false, 3206 + "asc": true, 3207 + "nulls": "last" 3208 + } 3209 + ], 3210 + "isUnique": false, 3211 + "concurrently": false, 3212 + "method": "btree", 3213 + "with": {} 3214 + }, 3215 + "ozone_labels_val_idx": { 3216 + "name": "ozone_labels_val_idx", 3217 + "columns": [ 3218 + { 3219 + "expression": "val", 3220 + "isExpression": false, 3221 + "asc": true, 3222 + "nulls": "last" 3223 + } 3224 + ], 3225 + "isUnique": false, 3226 + "concurrently": false, 3227 + "method": "btree", 3228 + "with": {} 3229 + }, 3230 + "ozone_labels_indexed_at_idx": { 3231 + "name": "ozone_labels_indexed_at_idx", 3232 + "columns": [ 3233 + { 3234 + "expression": "indexed_at", 3235 + "isExpression": false, 3236 + "asc": true, 3237 + "nulls": "last" 3238 + } 3239 + ], 3240 + "isUnique": false, 3241 + "concurrently": false, 3242 + "method": "btree", 3243 + "with": {} 3244 + } 3245 + }, 3246 + "foreignKeys": {}, 3247 + "compositePrimaryKeys": {}, 3248 + "uniqueConstraints": {}, 3249 + "policies": {}, 3250 + "checkConstraints": {}, 3251 + "isRLSEnabled": false 3252 + }, 3253 + "public.community_profiles": { 3254 + "name": "community_profiles", 3255 + "schema": "", 3256 + "columns": { 3257 + "did": { 3258 + "name": "did", 3259 + "type": "text", 3260 + "primaryKey": false, 3261 + "notNull": true 3262 + }, 3263 + "community_did": { 3264 + "name": "community_did", 3265 + "type": "text", 3266 + "primaryKey": false, 3267 + "notNull": true 3268 + }, 3269 + "display_name": { 3270 + "name": "display_name", 3271 + "type": "text", 3272 + "primaryKey": false, 3273 + "notNull": false 3274 + }, 3275 + "avatar_url": { 3276 + "name": "avatar_url", 3277 + "type": "text", 3278 + "primaryKey": false, 3279 + "notNull": false 3280 + }, 3281 + "banner_url": { 3282 + "name": "banner_url", 3283 + "type": "text", 3284 + "primaryKey": false, 3285 + "notNull": false 3286 + }, 3287 + "bio": { 3288 + "name": "bio", 3289 + "type": "text", 3290 + "primaryKey": false, 3291 + "notNull": false 3292 + }, 3293 + "updated_at": { 3294 + "name": "updated_at", 3295 + "type": "timestamp with time zone", 3296 + "primaryKey": false, 3297 + "notNull": true, 3298 + "default": "now()" 3299 + } 3300 + }, 3301 + "indexes": { 3302 + "community_profiles_did_idx": { 3303 + "name": "community_profiles_did_idx", 3304 + "columns": [ 3305 + { 3306 + "expression": "did", 3307 + "isExpression": false, 3308 + "asc": true, 3309 + "nulls": "last" 3310 + } 3311 + ], 3312 + "isUnique": false, 3313 + "concurrently": false, 3314 + "method": "btree", 3315 + "with": {} 3316 + }, 3317 + "community_profiles_community_idx": { 3318 + "name": "community_profiles_community_idx", 3319 + "columns": [ 3320 + { 3321 + "expression": "community_did", 3322 + "isExpression": false, 3323 + "asc": true, 3324 + "nulls": "last" 3325 + } 3326 + ], 3327 + "isUnique": false, 3328 + "concurrently": false, 3329 + "method": "btree", 3330 + "with": {} 3331 + } 3332 + }, 3333 + "foreignKeys": {}, 3334 + "compositePrimaryKeys": { 3335 + "community_profiles_did_community_did_pk": { 3336 + "name": "community_profiles_did_community_did_pk", 3337 + "columns": [ 3338 + "did", 3339 + "community_did" 3340 + ] 3341 + } 3342 + }, 3343 + "uniqueConstraints": {}, 3344 + "policies": { 3345 + "tenant_isolation": { 3346 + "name": "tenant_isolation", 3347 + "as": "PERMISSIVE", 3348 + "for": "ALL", 3349 + "to": [ 3350 + "barazo_app" 3351 + ], 3352 + "using": "community_did = current_setting('app.current_community_did', true)", 3353 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 3354 + } 3355 + }, 3356 + "checkConstraints": {}, 3357 + "isRLSEnabled": true 3358 + }, 3359 + "public.interaction_graph": { 3360 + "name": "interaction_graph", 3361 + "schema": "", 3362 + "columns": { 3363 + "source_did": { 3364 + "name": "source_did", 3365 + "type": "text", 3366 + "primaryKey": false, 3367 + "notNull": true 3368 + }, 3369 + "target_did": { 3370 + "name": "target_did", 3371 + "type": "text", 3372 + "primaryKey": false, 3373 + "notNull": true 3374 + }, 3375 + "community_id": { 3376 + "name": "community_id", 3377 + "type": "text", 3378 + "primaryKey": false, 3379 + "notNull": true 3380 + }, 3381 + "interaction_type": { 3382 + "name": "interaction_type", 3383 + "type": "text", 3384 + "primaryKey": false, 3385 + "notNull": true 3386 + }, 3387 + "weight": { 3388 + "name": "weight", 3389 + "type": "integer", 3390 + "primaryKey": false, 3391 + "notNull": true, 3392 + "default": 1 3393 + }, 3394 + "first_interaction_at": { 3395 + "name": "first_interaction_at", 3396 + "type": "timestamp with time zone", 3397 + "primaryKey": false, 3398 + "notNull": true, 3399 + "default": "now()" 3400 + }, 3401 + "last_interaction_at": { 3402 + "name": "last_interaction_at", 3403 + "type": "timestamp with time zone", 3404 + "primaryKey": false, 3405 + "notNull": true, 3406 + "default": "now()" 3407 + } 3408 + }, 3409 + "indexes": { 3410 + "interaction_graph_source_target_community_idx": { 3411 + "name": "interaction_graph_source_target_community_idx", 3412 + "columns": [ 3413 + { 3414 + "expression": "source_did", 3415 + "isExpression": false, 3416 + "asc": true, 3417 + "nulls": "last" 3418 + }, 3419 + { 3420 + "expression": "target_did", 3421 + "isExpression": false, 3422 + "asc": true, 3423 + "nulls": "last" 3424 + }, 3425 + { 3426 + "expression": "community_id", 3427 + "isExpression": false, 3428 + "asc": true, 3429 + "nulls": "last" 3430 + } 3431 + ], 3432 + "isUnique": false, 3433 + "concurrently": false, 3434 + "method": "btree", 3435 + "with": {} 3436 + } 3437 + }, 3438 + "foreignKeys": {}, 3439 + "compositePrimaryKeys": { 3440 + "interaction_graph_source_did_target_did_community_id_interaction_type_pk": { 3441 + "name": "interaction_graph_source_did_target_did_community_id_interaction_type_pk", 3442 + "columns": [ 3443 + "source_did", 3444 + "target_did", 3445 + "community_id", 3446 + "interaction_type" 3447 + ] 3448 + } 3449 + }, 3450 + "uniqueConstraints": {}, 3451 + "policies": {}, 3452 + "checkConstraints": {}, 3453 + "isRLSEnabled": false 3454 + }, 3455 + "public.trust_seeds": { 3456 + "name": "trust_seeds", 3457 + "schema": "", 3458 + "columns": { 3459 + "id": { 3460 + "name": "id", 3461 + "type": "serial", 3462 + "primaryKey": true, 3463 + "notNull": true 3464 + }, 3465 + "did": { 3466 + "name": "did", 3467 + "type": "text", 3468 + "primaryKey": false, 3469 + "notNull": true 3470 + }, 3471 + "community_id": { 3472 + "name": "community_id", 3473 + "type": "text", 3474 + "primaryKey": false, 3475 + "notNull": true, 3476 + "default": "''" 3477 + }, 3478 + "added_by": { 3479 + "name": "added_by", 3480 + "type": "text", 3481 + "primaryKey": false, 3482 + "notNull": true 3483 + }, 3484 + "reason": { 3485 + "name": "reason", 3486 + "type": "text", 3487 + "primaryKey": false, 3488 + "notNull": false 3489 + }, 3490 + "created_at": { 3491 + "name": "created_at", 3492 + "type": "timestamp with time zone", 3493 + "primaryKey": false, 3494 + "notNull": true, 3495 + "default": "now()" 3496 + } 3497 + }, 3498 + "indexes": { 3499 + "trust_seeds_did_community_idx": { 3500 + "name": "trust_seeds_did_community_idx", 3501 + "columns": [ 3502 + { 3503 + "expression": "did", 3504 + "isExpression": false, 3505 + "asc": true, 3506 + "nulls": "last" 3507 + }, 3508 + { 3509 + "expression": "community_id", 3510 + "isExpression": false, 3511 + "asc": true, 3512 + "nulls": "last" 3513 + } 3514 + ], 3515 + "isUnique": true, 3516 + "concurrently": false, 3517 + "method": "btree", 3518 + "with": {} 3519 + } 3520 + }, 3521 + "foreignKeys": {}, 3522 + "compositePrimaryKeys": {}, 3523 + "uniqueConstraints": {}, 3524 + "policies": {}, 3525 + "checkConstraints": {}, 3526 + "isRLSEnabled": false 3527 + }, 3528 + "public.trust_scores": { 3529 + "name": "trust_scores", 3530 + "schema": "", 3531 + "columns": { 3532 + "did": { 3533 + "name": "did", 3534 + "type": "text", 3535 + "primaryKey": false, 3536 + "notNull": true 3537 + }, 3538 + "community_id": { 3539 + "name": "community_id", 3540 + "type": "text", 3541 + "primaryKey": false, 3542 + "notNull": true, 3543 + "default": "''" 3544 + }, 3545 + "score": { 3546 + "name": "score", 3547 + "type": "real", 3548 + "primaryKey": false, 3549 + "notNull": true 3550 + }, 3551 + "computed_at": { 3552 + "name": "computed_at", 3553 + "type": "timestamp with time zone", 3554 + "primaryKey": false, 3555 + "notNull": true, 3556 + "default": "now()" 3557 + } 3558 + }, 3559 + "indexes": { 3560 + "trust_scores_did_community_idx": { 3561 + "name": "trust_scores_did_community_idx", 3562 + "columns": [ 3563 + { 3564 + "expression": "did", 3565 + "isExpression": false, 3566 + "asc": true, 3567 + "nulls": "last" 3568 + }, 3569 + { 3570 + "expression": "community_id", 3571 + "isExpression": false, 3572 + "asc": true, 3573 + "nulls": "last" 3574 + } 3575 + ], 3576 + "isUnique": false, 3577 + "concurrently": false, 3578 + "method": "btree", 3579 + "with": {} 3580 + } 3581 + }, 3582 + "foreignKeys": {}, 3583 + "compositePrimaryKeys": { 3584 + "trust_scores_did_community_id_pk": { 3585 + "name": "trust_scores_did_community_id_pk", 3586 + "columns": [ 3587 + "did", 3588 + "community_id" 3589 + ] 3590 + } 3591 + }, 3592 + "uniqueConstraints": {}, 3593 + "policies": {}, 3594 + "checkConstraints": {}, 3595 + "isRLSEnabled": false 3596 + }, 3597 + "public.sybil_clusters": { 3598 + "name": "sybil_clusters", 3599 + "schema": "", 3600 + "columns": { 3601 + "id": { 3602 + "name": "id", 3603 + "type": "serial", 3604 + "primaryKey": true, 3605 + "notNull": true 3606 + }, 3607 + "cluster_hash": { 3608 + "name": "cluster_hash", 3609 + "type": "text", 3610 + "primaryKey": false, 3611 + "notNull": true 3612 + }, 3613 + "internal_edge_count": { 3614 + "name": "internal_edge_count", 3615 + "type": "integer", 3616 + "primaryKey": false, 3617 + "notNull": true 3618 + }, 3619 + "external_edge_count": { 3620 + "name": "external_edge_count", 3621 + "type": "integer", 3622 + "primaryKey": false, 3623 + "notNull": true 3624 + }, 3625 + "member_count": { 3626 + "name": "member_count", 3627 + "type": "integer", 3628 + "primaryKey": false, 3629 + "notNull": true 3630 + }, 3631 + "status": { 3632 + "name": "status", 3633 + "type": "text", 3634 + "primaryKey": false, 3635 + "notNull": true, 3636 + "default": "'flagged'" 3637 + }, 3638 + "reviewed_by": { 3639 + "name": "reviewed_by", 3640 + "type": "text", 3641 + "primaryKey": false, 3642 + "notNull": false 3643 + }, 3644 + "reviewed_at": { 3645 + "name": "reviewed_at", 3646 + "type": "timestamp with time zone", 3647 + "primaryKey": false, 3648 + "notNull": false 3649 + }, 3650 + "detected_at": { 3651 + "name": "detected_at", 3652 + "type": "timestamp with time zone", 3653 + "primaryKey": false, 3654 + "notNull": true, 3655 + "default": "now()" 3656 + }, 3657 + "updated_at": { 3658 + "name": "updated_at", 3659 + "type": "timestamp with time zone", 3660 + "primaryKey": false, 3661 + "notNull": true, 3662 + "default": "now()" 3663 + } 3664 + }, 3665 + "indexes": { 3666 + "sybil_clusters_hash_idx": { 3667 + "name": "sybil_clusters_hash_idx", 3668 + "columns": [ 3669 + { 3670 + "expression": "cluster_hash", 3671 + "isExpression": false, 3672 + "asc": true, 3673 + "nulls": "last" 3674 + } 3675 + ], 3676 + "isUnique": true, 3677 + "concurrently": false, 3678 + "method": "btree", 3679 + "with": {} 3680 + } 3681 + }, 3682 + "foreignKeys": {}, 3683 + "compositePrimaryKeys": {}, 3684 + "uniqueConstraints": {}, 3685 + "policies": {}, 3686 + "checkConstraints": {}, 3687 + "isRLSEnabled": false 3688 + }, 3689 + "public.sybil_cluster_members": { 3690 + "name": "sybil_cluster_members", 3691 + "schema": "", 3692 + "columns": { 3693 + "cluster_id": { 3694 + "name": "cluster_id", 3695 + "type": "integer", 3696 + "primaryKey": false, 3697 + "notNull": true 3698 + }, 3699 + "did": { 3700 + "name": "did", 3701 + "type": "text", 3702 + "primaryKey": false, 3703 + "notNull": true 3704 + }, 3705 + "role_in_cluster": { 3706 + "name": "role_in_cluster", 3707 + "type": "text", 3708 + "primaryKey": false, 3709 + "notNull": true 3710 + }, 3711 + "joined_at": { 3712 + "name": "joined_at", 3713 + "type": "timestamp with time zone", 3714 + "primaryKey": false, 3715 + "notNull": true, 3716 + "default": "now()" 3717 + } 3718 + }, 3719 + "indexes": {}, 3720 + "foreignKeys": { 3721 + "sybil_cluster_members_cluster_id_sybil_clusters_id_fk": { 3722 + "name": "sybil_cluster_members_cluster_id_sybil_clusters_id_fk", 3723 + "tableFrom": "sybil_cluster_members", 3724 + "tableTo": "sybil_clusters", 3725 + "columnsFrom": [ 3726 + "cluster_id" 3727 + ], 3728 + "columnsTo": [ 3729 + "id" 3730 + ], 3731 + "onDelete": "no action", 3732 + "onUpdate": "no action" 3733 + } 3734 + }, 3735 + "compositePrimaryKeys": { 3736 + "sybil_cluster_members_cluster_id_did_pk": { 3737 + "name": "sybil_cluster_members_cluster_id_did_pk", 3738 + "columns": [ 3739 + "cluster_id", 3740 + "did" 3741 + ] 3742 + } 3743 + }, 3744 + "uniqueConstraints": {}, 3745 + "policies": {}, 3746 + "checkConstraints": {}, 3747 + "isRLSEnabled": false 3748 + }, 3749 + "public.behavioral_flags": { 3750 + "name": "behavioral_flags", 3751 + "schema": "", 3752 + "columns": { 3753 + "id": { 3754 + "name": "id", 3755 + "type": "serial", 3756 + "primaryKey": true, 3757 + "notNull": true 3758 + }, 3759 + "flag_type": { 3760 + "name": "flag_type", 3761 + "type": "text", 3762 + "primaryKey": false, 3763 + "notNull": true 3764 + }, 3765 + "affected_dids": { 3766 + "name": "affected_dids", 3767 + "type": "jsonb", 3768 + "primaryKey": false, 3769 + "notNull": true 3770 + }, 3771 + "details": { 3772 + "name": "details", 3773 + "type": "text", 3774 + "primaryKey": false, 3775 + "notNull": true 3776 + }, 3777 + "community_did": { 3778 + "name": "community_did", 3779 + "type": "text", 3780 + "primaryKey": false, 3781 + "notNull": false 3782 + }, 3783 + "status": { 3784 + "name": "status", 3785 + "type": "text", 3786 + "primaryKey": false, 3787 + "notNull": true, 3788 + "default": "'pending'" 3789 + }, 3790 + "detected_at": { 3791 + "name": "detected_at", 3792 + "type": "timestamp with time zone", 3793 + "primaryKey": false, 3794 + "notNull": true, 3795 + "default": "now()" 3796 + } 3797 + }, 3798 + "indexes": { 3799 + "behavioral_flags_flag_type_idx": { 3800 + "name": "behavioral_flags_flag_type_idx", 3801 + "columns": [ 3802 + { 3803 + "expression": "flag_type", 3804 + "isExpression": false, 3805 + "asc": true, 3806 + "nulls": "last" 3807 + } 3808 + ], 3809 + "isUnique": false, 3810 + "concurrently": false, 3811 + "method": "btree", 3812 + "with": {} 3813 + }, 3814 + "behavioral_flags_status_idx": { 3815 + "name": "behavioral_flags_status_idx", 3816 + "columns": [ 3817 + { 3818 + "expression": "status", 3819 + "isExpression": false, 3820 + "asc": true, 3821 + "nulls": "last" 3822 + } 3823 + ], 3824 + "isUnique": false, 3825 + "concurrently": false, 3826 + "method": "btree", 3827 + "with": {} 3828 + }, 3829 + "behavioral_flags_detected_at_idx": { 3830 + "name": "behavioral_flags_detected_at_idx", 3831 + "columns": [ 3832 + { 3833 + "expression": "detected_at", 3834 + "isExpression": false, 3835 + "asc": true, 3836 + "nulls": "last" 3837 + } 3838 + ], 3839 + "isUnique": false, 3840 + "concurrently": false, 3841 + "method": "btree", 3842 + "with": {} 3843 + } 3844 + }, 3845 + "foreignKeys": {}, 3846 + "compositePrimaryKeys": {}, 3847 + "uniqueConstraints": {}, 3848 + "policies": {}, 3849 + "checkConstraints": {}, 3850 + "isRLSEnabled": false 3851 + }, 3852 + "public.pds_trust_factors": { 3853 + "name": "pds_trust_factors", 3854 + "schema": "", 3855 + "columns": { 3856 + "id": { 3857 + "name": "id", 3858 + "type": "serial", 3859 + "primaryKey": true, 3860 + "notNull": true 3861 + }, 3862 + "pds_host": { 3863 + "name": "pds_host", 3864 + "type": "text", 3865 + "primaryKey": false, 3866 + "notNull": true 3867 + }, 3868 + "trust_factor": { 3869 + "name": "trust_factor", 3870 + "type": "real", 3871 + "primaryKey": false, 3872 + "notNull": true 3873 + }, 3874 + "is_default": { 3875 + "name": "is_default", 3876 + "type": "boolean", 3877 + "primaryKey": false, 3878 + "notNull": true, 3879 + "default": false 3880 + }, 3881 + "updated_at": { 3882 + "name": "updated_at", 3883 + "type": "timestamp with time zone", 3884 + "primaryKey": false, 3885 + "notNull": true, 3886 + "default": "now()" 3887 + } 3888 + }, 3889 + "indexes": { 3890 + "pds_trust_factors_pds_host_idx": { 3891 + "name": "pds_trust_factors_pds_host_idx", 3892 + "columns": [ 3893 + { 3894 + "expression": "pds_host", 3895 + "isExpression": false, 3896 + "asc": true, 3897 + "nulls": "last" 3898 + } 3899 + ], 3900 + "isUnique": true, 3901 + "concurrently": false, 3902 + "method": "btree", 3903 + "with": {} 3904 + } 3905 + }, 3906 + "foreignKeys": {}, 3907 + "compositePrimaryKeys": {}, 3908 + "uniqueConstraints": {}, 3909 + "policies": {}, 3910 + "checkConstraints": {}, 3911 + "isRLSEnabled": false 3912 + }, 3913 + "public.pages": { 3914 + "name": "pages", 3915 + "schema": "", 3916 + "columns": { 3917 + "id": { 3918 + "name": "id", 3919 + "type": "text", 3920 + "primaryKey": true, 3921 + "notNull": true 3922 + }, 3923 + "slug": { 3924 + "name": "slug", 3925 + "type": "text", 3926 + "primaryKey": false, 3927 + "notNull": true 3928 + }, 3929 + "title": { 3930 + "name": "title", 3931 + "type": "text", 3932 + "primaryKey": false, 3933 + "notNull": true 3934 + }, 3935 + "content": { 3936 + "name": "content", 3937 + "type": "text", 3938 + "primaryKey": false, 3939 + "notNull": true 3940 + }, 3941 + "status": { 3942 + "name": "status", 3943 + "type": "text", 3944 + "primaryKey": false, 3945 + "notNull": true, 3946 + "default": "'draft'" 3947 + }, 3948 + "meta_description": { 3949 + "name": "meta_description", 3950 + "type": "text", 3951 + "primaryKey": false, 3952 + "notNull": false 3953 + }, 3954 + "parent_id": { 3955 + "name": "parent_id", 3956 + "type": "text", 3957 + "primaryKey": false, 3958 + "notNull": false 3959 + }, 3960 + "sort_order": { 3961 + "name": "sort_order", 3962 + "type": "integer", 3963 + "primaryKey": false, 3964 + "notNull": true, 3965 + "default": 0 3966 + }, 3967 + "community_did": { 3968 + "name": "community_did", 3969 + "type": "text", 3970 + "primaryKey": false, 3971 + "notNull": true 3972 + }, 3973 + "created_at": { 3974 + "name": "created_at", 3975 + "type": "timestamp with time zone", 3976 + "primaryKey": false, 3977 + "notNull": true, 3978 + "default": "now()" 3979 + }, 3980 + "updated_at": { 3981 + "name": "updated_at", 3982 + "type": "timestamp with time zone", 3983 + "primaryKey": false, 3984 + "notNull": true, 3985 + "default": "now()" 3986 + } 3987 + }, 3988 + "indexes": { 3989 + "pages_slug_community_did_idx": { 3990 + "name": "pages_slug_community_did_idx", 3991 + "columns": [ 3992 + { 3993 + "expression": "slug", 3994 + "isExpression": false, 3995 + "asc": true, 3996 + "nulls": "last" 3997 + }, 3998 + { 3999 + "expression": "community_did", 4000 + "isExpression": false, 4001 + "asc": true, 4002 + "nulls": "last" 4003 + } 4004 + ], 4005 + "isUnique": true, 4006 + "concurrently": false, 4007 + "method": "btree", 4008 + "with": {} 4009 + }, 4010 + "pages_community_did_idx": { 4011 + "name": "pages_community_did_idx", 4012 + "columns": [ 4013 + { 4014 + "expression": "community_did", 4015 + "isExpression": false, 4016 + "asc": true, 4017 + "nulls": "last" 4018 + } 4019 + ], 4020 + "isUnique": false, 4021 + "concurrently": false, 4022 + "method": "btree", 4023 + "with": {} 4024 + }, 4025 + "pages_parent_id_idx": { 4026 + "name": "pages_parent_id_idx", 4027 + "columns": [ 4028 + { 4029 + "expression": "parent_id", 4030 + "isExpression": false, 4031 + "asc": true, 4032 + "nulls": "last" 4033 + } 4034 + ], 4035 + "isUnique": false, 4036 + "concurrently": false, 4037 + "method": "btree", 4038 + "with": {} 4039 + }, 4040 + "pages_status_community_did_idx": { 4041 + "name": "pages_status_community_did_idx", 4042 + "columns": [ 4043 + { 4044 + "expression": "status", 4045 + "isExpression": false, 4046 + "asc": true, 4047 + "nulls": "last" 4048 + }, 4049 + { 4050 + "expression": "community_did", 4051 + "isExpression": false, 4052 + "asc": true, 4053 + "nulls": "last" 4054 + } 4055 + ], 4056 + "isUnique": false, 4057 + "concurrently": false, 4058 + "method": "btree", 4059 + "with": {} 4060 + } 4061 + }, 4062 + "foreignKeys": { 4063 + "pages_parent_id_fk": { 4064 + "name": "pages_parent_id_fk", 4065 + "tableFrom": "pages", 4066 + "tableTo": "pages", 4067 + "columnsFrom": [ 4068 + "parent_id" 4069 + ], 4070 + "columnsTo": [ 4071 + "id" 4072 + ], 4073 + "onDelete": "set null", 4074 + "onUpdate": "no action" 4075 + } 4076 + }, 4077 + "compositePrimaryKeys": {}, 4078 + "uniqueConstraints": {}, 4079 + "policies": { 4080 + "tenant_isolation": { 4081 + "name": "tenant_isolation", 4082 + "as": "PERMISSIVE", 4083 + "for": "ALL", 4084 + "to": [ 4085 + "barazo_app" 4086 + ], 4087 + "using": "community_did = current_setting('app.current_community_did', true)", 4088 + "withCheck": "community_did = current_setting('app.current_community_did', true)" 4089 + } 4090 + }, 4091 + "checkConstraints": {}, 4092 + "isRLSEnabled": true 4093 + } 4094 + }, 4095 + "enums": {}, 4096 + "schemas": {}, 4097 + "sequences": {}, 4098 + "roles": { 4099 + "barazo_app": { 4100 + "name": "barazo_app", 4101 + "createDb": false, 4102 + "createRole": false, 4103 + "inherit": true 4104 + } 4105 + }, 4106 + "policies": {}, 4107 + "views": {}, 4108 + "_meta": { 4109 + "columns": {}, 4110 + "schemas": {}, 4111 + "tables": {} 4112 + } 4113 + }
+7
drizzle/meta/_journal.json
··· 57 57 "when": 1772636063890, 58 58 "tag": "0007_ordinary_mulholland_black", 59 59 "breakpoints": true 60 + }, 61 + { 62 + "idx": 8, 63 + "version": "7", 64 + "when": 1772706380033, 65 + "tag": "0008_add_author_rkey_indexes", 66 + "breakpoints": true 60 67 } 61 68 ] 62 69 }
+1
src/db/schema/replies.ts
··· 59 59 index('replies_trust_status_idx').on(table.trustStatus), 60 60 index('replies_root_uri_created_at_idx').on(table.rootUri, table.createdAt), 61 61 index('replies_root_uri_depth_idx').on(table.rootUri, table.depth), 62 + index('replies_author_did_rkey_idx').on(table.authorDid, table.rkey), 62 63 pgPolicy('tenant_isolation', { 63 64 as: 'permissive', 64 65 to: appRole,
+1
src/db/schema/topics.ts
··· 55 55 table.category, 56 56 table.lastActivityAt 57 57 ), 58 + index('topics_author_did_rkey_idx').on(table.authorDid, table.rkey), 58 59 pgPolicy('tenant_isolation', { 59 60 as: 'permissive', 60 61 to: appRole,
+41
src/lib/resolve-handle-to-did.ts
··· 1 + import { eq } from 'drizzle-orm' 2 + import type { Database } from '../db/index.js' 3 + import type { Logger } from './logger.js' 4 + import { users } from '../db/schema/users.js' 5 + 6 + /** 7 + * Resolve an AT Protocol handle to a DID. 8 + * 9 + * 1. Check the local users table first (fast path). 10 + * 2. Fall back to the public Bluesky AppView XRPC endpoint. 11 + * 3. Return `null` if resolution fails. 12 + */ 13 + export async function resolveHandleToDid( 14 + handle: string, 15 + db: Database, 16 + logger: Logger 17 + ): Promise<string | null> { 18 + const localRows = await db 19 + .select({ did: users.did }) 20 + .from(users) 21 + .where(eq(users.handle, handle)) 22 + 23 + if (localRows[0]) { 24 + return localRows[0].did 25 + } 26 + 27 + try { 28 + const url = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}` 29 + const res = await fetch(url, { signal: AbortSignal.timeout(5000) }) 30 + if (res.ok) { 31 + const data = (await res.json()) as { did?: string } 32 + if (data.did) { 33 + return data.did 34 + } 35 + } 36 + } catch { 37 + logger.warn({ handle }, 'Failed to resolve handle via Bluesky AppView') 38 + } 39 + 40 + return null 41 + }
+147 -2
src/routes/notifications.ts
··· 1 - import { eq, and, sql, desc } from 'drizzle-orm' 1 + import { eq, and, sql, desc, inArray } from 'drizzle-orm' 2 2 import type { FastifyPluginCallback } from 'fastify' 3 3 import { badRequest, errorResponseSchema } from '../lib/api-errors.js' 4 4 import { notificationQuerySchema, markReadSchema } from '../validation/notifications.js' 5 5 import { notifications } from '../db/schema/notifications.js' 6 + import { users } from '../db/schema/users.js' 7 + import { topics } from '../db/schema/topics.js' 8 + import { replies } from '../db/schema/replies.js' 9 + import { getCollectionFromUri } from '../lib/at-uri.js' 6 10 7 11 // --------------------------------------------------------------------------- 8 12 // OpenAPI JSON Schema definitions ··· 15 19 type: { type: 'string' as const }, 16 20 subjectUri: { type: 'string' as const }, 17 21 actorDid: { type: 'string' as const }, 22 + actorHandle: { type: ['string', 'null'] as const }, 23 + subjectTitle: { type: ['string', 'null'] as const }, 24 + subjectAuthorDid: { type: ['string', 'null'] as const }, 25 + subjectAuthorHandle: { type: ['string', 'null'] as const }, 26 + message: { type: ['string', 'null'] as const }, 18 27 communityDid: { type: 'string' as const }, 19 28 read: { type: 'boolean' as const }, 20 29 createdAt: { type: 'string' as const, format: 'date-time' as const }, ··· 38 47 communityDid: row.communityDid, 39 48 read: row.read, 40 49 createdAt: row.createdAt.toISOString(), 50 + } 51 + } 52 + 53 + /** 54 + * Build a human-readable notification message. 55 + */ 56 + function buildNotificationMessage( 57 + type: string, 58 + actorHandle: string | null, 59 + subjectTitle: string | null 60 + ): string | null { 61 + const actor = actorHandle ?? 'Someone' 62 + const subject = subjectTitle ? `"${subjectTitle}"` : 'your content' 63 + 64 + switch (type) { 65 + case 'reply': 66 + return `${actor} replied to ${subject}` 67 + case 'reaction': 68 + return `${actor} reacted to ${subject}` 69 + case 'mention': 70 + return `${actor} mentioned you in ${subject}` 71 + case 'mod_action': 72 + return `A moderator took action on ${subject}` 73 + case 'cross_post_failed': 74 + return `Cross-post failed for ${subject}` 75 + case 'cross_post_revoked': 76 + return `Cross-post authorization was revoked` 77 + default: 78 + return null 41 79 } 42 80 } 43 81 ··· 165 203 const resultRows = hasMore ? rows.slice(0, limit) : rows 166 204 const serialized = resultRows.map(serializeNotification) 167 205 206 + // Batch-resolve actor handles 207 + const actorDids = [...new Set(serialized.map((n) => n.actorDid))] 208 + const actorHandleMap = new Map<string, string>() 209 + if (actorDids.length > 0) { 210 + const actorRows = await db 211 + .select({ did: users.did, handle: users.handle }) 212 + .from(users) 213 + .where(inArray(users.did, actorDids)) 214 + for (const row of actorRows) { 215 + actorHandleMap.set(row.did, row.handle) 216 + } 217 + } 218 + 219 + // Batch-resolve subject titles and authors from topics and replies 220 + const subjectUris = [...new Set(serialized.map((n) => n.subjectUri))] 221 + const topicUris = subjectUris.filter( 222 + (uri) => getCollectionFromUri(uri) === 'forum.barazo.topic.post' 223 + ) 224 + const replyUris = subjectUris.filter( 225 + (uri) => getCollectionFromUri(uri) === 'forum.barazo.topic.reply' 226 + ) 227 + 228 + const subjectMap = new Map< 229 + string, 230 + { title: string | null; authorDid: string; authorHandle: string | null } 231 + >() 232 + 233 + if (topicUris.length > 0) { 234 + const topicRows = await db 235 + .select({ 236 + uri: topics.uri, 237 + title: topics.title, 238 + authorDid: topics.authorDid, 239 + }) 240 + .from(topics) 241 + .where(inArray(topics.uri, topicUris)) 242 + for (const row of topicRows) { 243 + subjectMap.set(row.uri, { 244 + title: row.title, 245 + authorDid: row.authorDid, 246 + authorHandle: null, 247 + }) 248 + } 249 + } 250 + 251 + if (replyUris.length > 0) { 252 + // For replies, look up the root topic title 253 + const replyRows = await db 254 + .select({ 255 + uri: replies.uri, 256 + authorDid: replies.authorDid, 257 + rootUri: replies.rootUri, 258 + }) 259 + .from(replies) 260 + .where(inArray(replies.uri, replyUris)) 261 + 262 + // Get root topic titles for replies 263 + const rootUris = [...new Set(replyRows.map((r) => r.rootUri))] 264 + const rootTitleMap = new Map<string, string>() 265 + if (rootUris.length > 0) { 266 + const rootRows = await db 267 + .select({ uri: topics.uri, title: topics.title }) 268 + .from(topics) 269 + .where(inArray(topics.uri, rootUris)) 270 + for (const row of rootRows) { 271 + rootTitleMap.set(row.uri, row.title) 272 + } 273 + } 274 + 275 + for (const row of replyRows) { 276 + subjectMap.set(row.uri, { 277 + title: rootTitleMap.get(row.rootUri) ?? null, 278 + authorDid: row.authorDid, 279 + authorHandle: null, 280 + }) 281 + } 282 + } 283 + 284 + // Resolve author handles for subjects 285 + const subjectAuthorDids = [ 286 + ...new Set([...subjectMap.values()].map((s) => s.authorDid)), 287 + ] 288 + if (subjectAuthorDids.length > 0) { 289 + const authorRows = await db 290 + .select({ did: users.did, handle: users.handle }) 291 + .from(users) 292 + .where(inArray(users.did, subjectAuthorDids)) 293 + const handleMap = new Map(authorRows.map((r) => [r.did, r.handle])) 294 + for (const [, subject] of subjectMap) { 295 + subject.authorHandle = handleMap.get(subject.authorDid) ?? null 296 + } 297 + } 298 + 299 + // Enrich notifications 300 + const enriched = serialized.map((n) => { 301 + const actorHandle = actorHandleMap.get(n.actorDid) ?? null 302 + const subject = subjectMap.get(n.subjectUri) 303 + return { 304 + ...n, 305 + actorHandle, 306 + subjectTitle: subject?.title ?? null, 307 + subjectAuthorDid: subject?.authorDid ?? null, 308 + subjectAuthorHandle: subject?.authorHandle ?? null, 309 + message: buildNotificationMessage(n.type, actorHandle, subject?.title ?? null), 310 + } 311 + }) 312 + 168 313 // Get total count for the user 169 314 const countResult = await db 170 315 .select({ count: sql<number>`count(*)::int` }) ··· 182 327 } 183 328 184 329 return reply.status(200).send({ 185 - notifications: serialized, 330 + notifications: enriched, 186 331 cursor: nextCursor, 187 332 total, 188 333 })
+53 -4
src/routes/replies.ts
··· 32 32 import { checkOnboardingComplete } from '../lib/onboarding-gate.js' 33 33 import { createNotificationService } from '../services/notification.js' 34 34 import { extractRkey } from '../lib/at-uri.js' 35 + import { resolveHandleToDid } from '../lib/resolve-handle-to-did.js' 35 36 36 37 // --------------------------------------------------------------------------- 37 38 // Constants ··· 163 164 /** 164 165 * Reply routes for the Barazo forum. 165 166 * 166 - * - POST /api/topics/:topicUri/replies -- Create a reply 167 - * - GET /api/topics/:topicUri/replies -- List replies for a topic 168 - * - PUT /api/replies/:uri -- Update a reply 169 - * - DELETE /api/replies/:uri -- Delete a reply 167 + * - POST /api/topics/:topicUri/replies -- Create a reply 168 + * - GET /api/topics/:topicUri/replies -- List replies for a topic 169 + * - GET /api/replies/by-author-rkey/:handle/:rkey -- Get a reply by author handle and rkey 170 + * - PUT /api/replies/:uri -- Update a reply 171 + * - DELETE /api/replies/:uri -- Delete a reply 170 172 */ 171 173 export function replyRoutes(): FastifyPluginCallback { 172 174 return (app, _opts, done) => { ··· 693 695 replies: annotatedReplies, 694 696 cursor: nextCursor, 695 697 }) 698 + } 699 + ) 700 + 701 + // ------------------------------------------------------------------- 702 + // GET /api/replies/by-author-rkey/:handle/:rkey (public, optionalAuth) 703 + // ------------------------------------------------------------------- 704 + 705 + app.get( 706 + '/api/replies/by-author-rkey/:handle/:rkey', 707 + { 708 + preHandler: [authMiddleware.optionalAuth], 709 + schema: { 710 + tags: ['Replies'], 711 + summary: 'Get a single reply by author handle and rkey', 712 + params: { 713 + type: 'object', 714 + required: ['handle', 'rkey'], 715 + properties: { 716 + handle: { type: 'string' }, 717 + rkey: { type: 'string' }, 718 + }, 719 + }, 720 + response: { 721 + 200: replyJsonSchema, 722 + 404: errorResponseSchema, 723 + }, 724 + }, 725 + }, 726 + async (request, reply) => { 727 + const { handle, rkey } = request.params as { handle: string; rkey: string } 728 + 729 + const did = await resolveHandleToDid(handle, db, app.log) 730 + if (!did) { 731 + throw notFound('User not found') 732 + } 733 + 734 + const rows = await db 735 + .select() 736 + .from(replies) 737 + .where(and(eq(replies.authorDid, did), eq(replies.rkey, rkey))) 738 + 739 + const row = rows[0] 740 + if (!row) { 741 + throw notFound('Reply not found') 742 + } 743 + 744 + return reply.status(200).send(serializeReply(row)) 696 745 } 697 746 ) 698 747
+79
src/routes/topics.ts
··· 33 33 import { checkOnboardingComplete } from '../lib/onboarding-gate.js' 34 34 import { createNotificationService } from '../services/notification.js' 35 35 import { extractRkey } from '../lib/at-uri.js' 36 + import { resolveHandleToDid } from '../lib/resolve-handle-to-did.js' 36 37 37 38 // --------------------------------------------------------------------------- 38 39 // Constants ··· 233 234 rkey: { type: 'string' }, 234 235 title: { type: 'string' }, 235 236 category: { type: 'string' }, 237 + authorHandle: { type: 'string' }, 236 238 moderationStatus: { type: 'string', enum: ['approved', 'held', 'rejected'] }, 237 239 createdAt: { type: 'string', format: 'date-time' }, 238 240 }, ··· 448 450 crossPostService 449 451 .crossPostTopic({ 450 452 did: user.did, 453 + handle: user.handle, 451 454 topicUri: pdsResult.uri, 452 455 title, 453 456 content, ··· 479 482 rkey, 480 483 title, 481 484 category, 485 + authorHandle: user.handle, 482 486 moderationStatus: contentModerationStatus, 483 487 createdAt: now, 484 488 }) ··· 843 847 avatarUrl: null, 844 848 }, 845 849 }) 850 + } 851 + ) 852 + 853 + // ------------------------------------------------------------------- 854 + // GET /api/topics/by-author-rkey/:handle/:rkey (public, optionalAuth) 855 + // ------------------------------------------------------------------- 856 + 857 + app.get( 858 + '/api/topics/by-author-rkey/:handle/:rkey', 859 + { 860 + preHandler: [authMiddleware.optionalAuth], 861 + schema: { 862 + tags: ['Topics'], 863 + summary: 'Get a single topic by author handle and rkey', 864 + params: { 865 + type: 'object', 866 + required: ['handle', 'rkey'], 867 + properties: { 868 + handle: { type: 'string' }, 869 + rkey: { type: 'string' }, 870 + }, 871 + }, 872 + response: { 873 + 200: topicJsonSchema, 874 + 403: errorResponseSchema, 875 + 404: errorResponseSchema, 876 + }, 877 + }, 878 + }, 879 + async (request, reply) => { 880 + const { handle, rkey } = request.params as { handle: string; rkey: string } 881 + 882 + const did = await resolveHandleToDid(handle, db, app.log) 883 + if (!did) { 884 + throw notFound('User not found') 885 + } 886 + 887 + const rows = await db 888 + .select() 889 + .from(topics) 890 + .where(and(eq(topics.authorDid, did), eq(topics.rkey, rkey))) 891 + 892 + const row = rows[0] 893 + if (!row) { 894 + throw notFound('Topic not found') 895 + } 896 + 897 + const communityDid = requireCommunityDid(request) 898 + const catRows = await db 899 + .select({ maturityRating: categories.maturityRating }) 900 + .from(categories) 901 + .where(and(eq(categories.slug, row.category), eq(categories.communityDid, communityDid))) 902 + const categoryRating = catRows[0]?.maturityRating ?? 'safe' 903 + 904 + let userProfile: MaturityUser | undefined 905 + if (request.user) { 906 + const userRows = await db 907 + .select({ declaredAge: users.declaredAge, maturityPref: users.maturityPref }) 908 + .from(users) 909 + .where(eq(users.did, request.user.did)) 910 + userProfile = userRows[0] ?? undefined 911 + } 912 + 913 + const authorRkeySettingsRows = await db 914 + .select({ ageThreshold: communitySettings.ageThreshold }) 915 + .from(communitySettings) 916 + .where(eq(communitySettings.communityDid, communityDid)) 917 + const authorRkeyAgeThreshold = authorRkeySettingsRows[0]?.ageThreshold ?? 16 918 + 919 + const maxMaturity = resolveMaxMaturity(userProfile, authorRkeyAgeThreshold) 920 + if (!maturityAllows(maxMaturity, categoryRating)) { 921 + throw forbidden('Content restricted by maturity settings') 922 + } 923 + 924 + return reply.status(200).send(serializeTopic(row, categoryRating)) 846 925 } 847 926 ) 848 927
+6 -5
src/services/cross-post.ts
··· 30 30 31 31 export interface CrossPostParams { 32 32 did: string 33 + handle: string 33 34 topicUri: string 34 35 title: string 35 36 content: string ··· 79 80 } 80 81 81 82 /** 82 - * Build the public URL for a topic from its AT URI. 83 + * Build the public URL for a topic using AT Protocol-style format. 83 84 */ 84 - function buildTopicUrl(publicUrl: string, topicUri: string): string { 85 + function buildTopicUrl(publicUrl: string, handle: string, topicUri: string): string { 85 86 const rkey = extractRkey(topicUri) 86 - return `${publicUrl}/topics/${rkey}` 87 + return `${publicUrl}/${handle}/${rkey}` 87 88 } 88 89 89 90 // --------------------------------------------------------------------------- ··· 136 137 * to the forum topic and a branded OG image thumbnail. 137 138 */ 138 139 async function crossPostToBluesky(params: CrossPostParams, thumb: unknown): Promise<void> { 139 - const topicUrl = buildTopicUrl(config.publicUrl, params.topicUri) 140 + const topicUrl = buildTopicUrl(config.publicUrl, params.handle, params.topicUri) 140 141 const postText = buildBlueskyPostText(params.title, params.content) 141 142 142 143 const external: Record<string, unknown> = { ··· 189 190 * (link submission pointing back to the forum topic). 190 191 */ 191 192 async function crossPostToFrontpage(params: CrossPostParams): Promise<void> { 192 - const topicUrl = buildTopicUrl(config.publicUrl, params.topicUri) 193 + const topicUrl = buildTopicUrl(config.publicUrl, params.handle, params.topicUri) 193 194 194 195 const record: Record<string, unknown> = { 195 196 title: params.title,
+25
tests/unit/routes/notifications.test.ts
··· 68 68 }) 69 69 } 70 70 71 + /** 72 + * Mock the enrichment queries that run after fetching notification rows. 73 + * These resolve actor handles and subject titles. Since mocked topic queries 74 + * return empty arrays, no subject author handle query is triggered. 75 + * Call this after `selectChain.limit.mockResolvedValueOnce(rows)` when 76 + * `rows` is non-empty (i.e., there are notifications to enrich). 77 + */ 78 + function mockEnrichmentQueries(): void { 79 + selectChain.where.mockResolvedValueOnce([]) // actor handles (users table) 80 + selectChain.where.mockResolvedValueOnce([]) // topic subjects (topics table) 81 + } 82 + 71 83 // --------------------------------------------------------------------------- 72 84 // Auth middleware mocks 73 85 // --------------------------------------------------------------------------- ··· 261 273 } 262 274 selectChain.where.mockReturnValueOnce(chainableThenable) 263 275 selectChain.limit.mockResolvedValueOnce([unreadNotification, readNotification]) 276 + mockEnrichmentQueries() 264 277 selectChain.where.mockResolvedValueOnce([{ count: 2 }]) 265 278 266 279 const response = await app.inject({ ··· 301 314 } 302 315 selectChain.where.mockReturnValueOnce(chainableThenable) 303 316 selectChain.limit.mockResolvedValueOnce(rows) 317 + mockEnrichmentQueries() 304 318 selectChain.where.mockResolvedValueOnce([{ count: 50 }]) 305 319 306 320 const response = await app.inject({ ··· 332 346 } 333 347 selectChain.where.mockReturnValueOnce(chainableThenable) 334 348 selectChain.limit.mockResolvedValueOnce(rows) 349 + mockEnrichmentQueries() 335 350 selectChain.where.mockResolvedValueOnce([{ count: 1 }]) 336 351 337 352 const response = await app.inject({ ··· 360 375 } 361 376 selectChain.where.mockReturnValueOnce(chainableThenable) 362 377 selectChain.limit.mockResolvedValueOnce([sampleNotificationRow()]) 378 + mockEnrichmentQueries() 363 379 selectChain.where.mockResolvedValueOnce([{ count: 1 }]) 364 380 365 381 const response = await app.inject({ ··· 423 439 } 424 440 selectChain.where.mockReturnValueOnce(chainableThenable) 425 441 selectChain.limit.mockResolvedValueOnce([unreadRow]) 442 + mockEnrichmentQueries() 426 443 selectChain.where.mockResolvedValueOnce([{ count: 1 }]) 427 444 428 445 const response = await app.inject({ ··· 454 471 } 455 472 selectChain.where.mockReturnValueOnce(chainableThenable) 456 473 selectChain.limit.mockResolvedValueOnce([unreadRow, readRow]) 474 + mockEnrichmentQueries() 457 475 selectChain.where.mockResolvedValueOnce([{ count: 2 }]) 458 476 459 477 const response = await app.inject({ ··· 484 502 } 485 503 selectChain.where.mockReturnValueOnce(chainableThenable) 486 504 selectChain.limit.mockResolvedValueOnce([unreadRow, readRow]) 505 + mockEnrichmentQueries() 487 506 selectChain.where.mockResolvedValueOnce([{ count: 2 }]) 488 507 489 508 const response = await app.inject({ ··· 522 541 } 523 542 selectChain.where.mockReturnValueOnce(chainableThenable) 524 543 selectChain.limit.mockResolvedValueOnce([row]) 544 + mockEnrichmentQueries() 525 545 selectChain.where.mockResolvedValueOnce([{ count: 10 }]) 526 546 527 547 const response = await app.inject({ ··· 559 579 } 560 580 selectChain.where.mockReturnValueOnce(chainableThenable) 561 581 selectChain.limit.mockResolvedValueOnce([row]) 582 + mockEnrichmentQueries() 562 583 selectChain.where.mockResolvedValueOnce([{ count: 5 }]) 563 584 564 585 const response = await app.inject({ ··· 776 797 } 777 798 selectChain.where.mockReturnValueOnce(chainableThenable) 778 799 selectChain.limit.mockResolvedValueOnce(rows) 800 + mockEnrichmentQueries() 779 801 selectChain.where.mockResolvedValueOnce([{ count: 25 }]) 780 802 781 803 const response = await app.inject({ ··· 811 833 } 812 834 selectChain.where.mockReturnValueOnce(chainableThenable) 813 835 selectChain.limit.mockResolvedValueOnce(rows) 836 + mockEnrichmentQueries() 814 837 selectChain.where.mockResolvedValueOnce([{ count: 5 }]) 815 838 816 839 const response = await app.inject({ ··· 847 870 } 848 871 selectChain.where.mockReturnValueOnce(chainableThenable) 849 872 selectChain.limit.mockResolvedValueOnce([row]) 873 + mockEnrichmentQueries() 850 874 selectChain.where.mockResolvedValueOnce([{ count: 5 }]) 851 875 852 876 const response = await app.inject({ ··· 891 915 } 892 916 selectChain.where.mockReturnValueOnce(chainableThenable) 893 917 selectChain.limit.mockResolvedValueOnce(rows) 918 + mockEnrichmentQueries() 894 919 selectChain.where.mockResolvedValueOnce([{ count: 100 }]) 895 920 896 921 const response = await app.inject({
+67
tests/unit/routes/replies.test.ts
··· 72 72 checkOnboardingComplete: (...args: unknown[]) => checkOnboardingCompleteFn(...args) as unknown, 73 73 })) 74 74 75 + // Mock handle-to-DID resolver 76 + const resolveHandleToDidFn = vi.fn<(handle: string) => Promise<string | null>>() 77 + vi.mock('../../../src/lib/resolve-handle-to-did.js', () => ({ 78 + resolveHandleToDid: (...args: unknown[]) => resolveHandleToDidFn(args[0] as string), 79 + })) 80 + 75 81 // Import routes AFTER mocking 76 82 import { replyRoutes } from '../../../src/routes/replies.js' 77 83 ··· 2560 2566 expect(response.statusCode).toBe(401) 2561 2567 const body = response.json<{ error: string }>() 2562 2568 expect(body.error).toBe('Authentication required') 2569 + }) 2570 + }) 2571 + 2572 + // ========================================================================= 2573 + // GET /api/replies/by-author-rkey/:handle/:rkey 2574 + // ========================================================================= 2575 + 2576 + describe('GET /api/replies/by-author-rkey/:handle/:rkey', () => { 2577 + let app: FastifyInstance 2578 + 2579 + beforeAll(async () => { 2580 + app = await buildTestApp(testUser()) 2581 + }) 2582 + 2583 + afterAll(async () => { 2584 + await app.close() 2585 + }) 2586 + 2587 + beforeEach(() => { 2588 + vi.clearAllMocks() 2589 + resetAllDbMocks() 2590 + }) 2591 + 2592 + it('returns a reply by author handle and rkey', async () => { 2593 + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) 2594 + const row = sampleReplyRow() 2595 + selectChain.where.mockResolvedValueOnce([row]) 2596 + 2597 + const response = await app.inject({ 2598 + method: 'GET', 2599 + url: `/api/replies/by-author-rkey/${TEST_HANDLE}/${TEST_REPLY_RKEY}`, 2600 + }) 2601 + 2602 + expect(response.statusCode).toBe(200) 2603 + const body = response.json<{ uri: string; rkey: string; content: string }>() 2604 + expect(body.uri).toBe(TEST_REPLY_URI) 2605 + expect(body.rkey).toBe(TEST_REPLY_RKEY) 2606 + expect(body.content).toBe('This is a test reply') 2607 + }) 2608 + 2609 + it('returns 404 when handle cannot be resolved', async () => { 2610 + resolveHandleToDidFn.mockResolvedValueOnce(null) 2611 + 2612 + const response = await app.inject({ 2613 + method: 'GET', 2614 + url: '/api/replies/by-author-rkey/unknown.handle/reply001', 2615 + }) 2616 + 2617 + expect(response.statusCode).toBe(404) 2618 + }) 2619 + 2620 + it('returns 404 when reply not found for author', async () => { 2621 + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) 2622 + selectChain.where.mockResolvedValueOnce([]) 2623 + 2624 + const response = await app.inject({ 2625 + method: 'GET', 2626 + url: `/api/replies/by-author-rkey/${TEST_HANDLE}/nonexistent`, 2627 + }) 2628 + 2629 + expect(response.statusCode).toBe(404) 2563 2630 }) 2564 2631 }) 2565 2632 })
+93 -1
tests/unit/routes/topics.test.ts
··· 62 62 }), 63 63 })) 64 64 65 + // Mock handle-to-DID resolver 66 + const resolveHandleToDidFn = vi.fn<(handle: string) => Promise<string | null>>() 67 + vi.mock('../../../src/lib/resolve-handle-to-did.js', () => ({ 68 + resolveHandleToDid: (...args: unknown[]) => resolveHandleToDidFn(args[0] as string), 69 + })) 70 + 65 71 // Mock anti-spam module (tested separately in anti-spam.test.ts) 66 72 const loadAntiSpamSettingsFn = vi.fn().mockResolvedValue({ 67 73 wordFilter: [], ··· 322 328 }) 323 329 324 330 expect(response.statusCode).toBe(201) 325 - const body = response.json<{ uri: string; cid: string }>() 331 + const body = response.json<{ uri: string; cid: string; authorHandle: string }>() 326 332 expect(body.uri).toBe(TEST_URI) 327 333 expect(body.cid).toBe(TEST_CID) 334 + expect(body.authorHandle).toBe(TEST_HANDLE) 328 335 329 336 // Should have called PDS createRecord 330 337 expect(createRecordFn).toHaveBeenCalledOnce() ··· 1203 1210 1204 1211 expect(response.statusCode).toBe(200) 1205 1212 1213 + await noAuthApp.close() 1214 + }) 1215 + }) 1216 + 1217 + // ========================================================================= 1218 + // GET /api/topics/by-author-rkey/:handle/:rkey 1219 + // ========================================================================= 1220 + 1221 + describe('GET /api/topics/by-author-rkey/:handle/:rkey', () => { 1222 + let app: FastifyInstance 1223 + 1224 + beforeAll(async () => { 1225 + app = await buildTestApp(testUser()) 1226 + }) 1227 + 1228 + afterAll(async () => { 1229 + await app.close() 1230 + }) 1231 + 1232 + beforeEach(() => { 1233 + vi.clearAllMocks() 1234 + resetAllDbMocks() 1235 + }) 1236 + 1237 + it('returns a topic by author handle and rkey', async () => { 1238 + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) 1239 + const row = sampleTopicRow() 1240 + // 1. select().from(topics).where(authorDid, rkey) -> find topic 1241 + selectChain.where.mockResolvedValueOnce([row]) 1242 + // 2. select(maturityRating).from(categories).where() -> category lookup 1243 + selectChain.where.mockResolvedValueOnce([{ maturityRating: 'safe' }]) 1244 + // 3. select(declaredAge, maturityPref).from(users).where() -> user profile 1245 + selectChain.where.mockResolvedValueOnce([{ declaredAge: null, maturityPref: 'safe' }]) 1246 + // 4. select(ageThreshold).from(communitySettings).where() -> age threshold 1247 + selectChain.where.mockResolvedValueOnce([{ ageThreshold: 16 }]) 1248 + 1249 + const response = await app.inject({ 1250 + method: 'GET', 1251 + url: `/api/topics/by-author-rkey/${TEST_HANDLE}/${TEST_RKEY}`, 1252 + }) 1253 + 1254 + expect(response.statusCode).toBe(200) 1255 + const body = response.json<{ uri: string; title: string }>() 1256 + expect(body.uri).toBe(TEST_URI) 1257 + expect(body.title).toBe('Test Topic Title') 1258 + }) 1259 + 1260 + it('returns 404 when handle cannot be resolved', async () => { 1261 + resolveHandleToDidFn.mockResolvedValueOnce(null) 1262 + 1263 + const response = await app.inject({ 1264 + method: 'GET', 1265 + url: '/api/topics/by-author-rkey/unknown.handle/abc123', 1266 + }) 1267 + 1268 + expect(response.statusCode).toBe(404) 1269 + }) 1270 + 1271 + it('returns 404 when topic not found for author', async () => { 1272 + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) 1273 + selectChain.where.mockResolvedValueOnce([]) // no topic found 1274 + 1275 + const response = await app.inject({ 1276 + method: 'GET', 1277 + url: `/api/topics/by-author-rkey/${TEST_HANDLE}/nonexistent`, 1278 + }) 1279 + 1280 + expect(response.statusCode).toBe(404) 1281 + }) 1282 + 1283 + it('returns 403 when maturity blocks access', async () => { 1284 + const noAuthApp = await buildTestApp(undefined) 1285 + 1286 + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) 1287 + const row = sampleTopicRow({ category: 'mature-cat' }) 1288 + selectChain.where.mockResolvedValueOnce([row]) 1289 + selectChain.where.mockResolvedValueOnce([{ maturityRating: 'mature' }]) 1290 + selectChain.where.mockResolvedValueOnce([{ ageThreshold: 16 }]) 1291 + 1292 + const response = await noAuthApp.inject({ 1293 + method: 'GET', 1294 + url: `/api/topics/by-author-rkey/${TEST_HANDLE}/${TEST_RKEY}`, 1295 + }) 1296 + 1297 + expect(response.statusCode).toBe(403) 1206 1298 await noAuthApp.close() 1207 1299 }) 1208 1300 })
+17 -2
tests/unit/services/cross-post.test.ts
··· 167 167 168 168 await service.crossPostTopic({ 169 169 did: TEST_DID, 170 + handle: 'test.handle', 170 171 topicUri: TEST_TOPIC_URI, 171 172 title: 'My Topic', 172 173 content: 'Topic content here.', ··· 218 219 219 220 await service.crossPostTopic({ 220 221 did: TEST_DID, 222 + handle: 'test.handle', 221 223 topicUri: TEST_TOPIC_URI, 222 224 title: 'OG Image Topic', 223 225 content: 'Testing OG image.', ··· 269 271 270 272 await service.crossPostTopic({ 271 273 did: TEST_DID, 274 + handle: 'test.handle', 272 275 topicUri: TEST_TOPIC_URI, 273 276 title: 'No Thumb Topic', 274 277 content: 'OG image will fail.', ··· 318 321 319 322 await service.crossPostTopic({ 320 323 did: TEST_DID, 324 + handle: 'test.handle', 321 325 topicUri: TEST_TOPIC_URI, 322 326 title: 'Upload Fail Topic', 323 327 content: 'Blob upload will fail.', ··· 359 363 360 364 await service.crossPostTopic({ 361 365 did: TEST_DID, 366 + handle: 'test.handle', 362 367 topicUri: TEST_TOPIC_URI, 363 368 title: 'Frontpage Topic', 364 369 content: 'Content for Frontpage.', ··· 375 380 expect(did).toBe(TEST_DID) 376 381 expect(collection).toBe('fyi.frontpage.post') 377 382 expect(record.title).toBe('Frontpage Topic') 378 - expect(record.url).toBe(`${TEST_PUBLIC_URL}/topics/abc123`) 383 + expect(record.url).toBe(`${TEST_PUBLIC_URL}/test.handle/abc123`) 379 384 380 385 expect(mockDb.insert).toHaveBeenCalledOnce() 381 386 }) ··· 406 411 407 412 await service.crossPostTopic({ 408 413 did: TEST_DID, 414 + handle: 'test.handle', 409 415 topicUri: TEST_TOPIC_URI, 410 416 title: 'Dual Post', 411 417 content: 'Content for both.', ··· 437 443 438 444 await service.crossPostTopic({ 439 445 did: TEST_DID, 446 + handle: 'test.handle', 440 447 topicUri: TEST_TOPIC_URI, 441 448 title: 'No Cross-Post', 442 449 content: 'Should not go anywhere.', ··· 469 476 470 477 await service.crossPostTopic({ 471 478 did: TEST_DID, 479 + handle: 'test.handle', 472 480 topicUri: TEST_TOPIC_URI, 473 481 title: 'No Scopes', 474 482 content: 'User has not authorized.', ··· 501 509 502 510 await service.crossPostTopic({ 503 511 did: TEST_DID, 512 + handle: 'test.handle', 504 513 topicUri: TEST_TOPIC_URI, 505 514 title: 'Will Fail', 506 515 content: 'Bluesky is down.', ··· 547 556 548 557 await service.crossPostTopic({ 549 558 did: TEST_DID, 559 + handle: 'test.handle', 550 560 topicUri: TEST_TOPIC_URI, 551 561 title: 'FP Fail', 552 562 content: 'Frontpage is down.', ··· 580 590 581 591 await service.crossPostTopic({ 582 592 did: TEST_DID, 593 + handle: 'test.handle', 583 594 topicUri: TEST_TOPIC_URI, 584 595 title: 'Both Fail', 585 596 content: 'Everything is broken.', ··· 620 631 621 632 await service.crossPostTopic({ 622 633 did: TEST_DID, 634 + handle: 'test.handle', 623 635 topicUri: TEST_TOPIC_URI, 624 636 title: 'Partial Success', 625 637 content: 'One succeeds, one fails.', ··· 674 686 675 687 await service.crossPostTopic({ 676 688 did: TEST_DID, 689 + handle: 'test.handle', 677 690 topicUri: TEST_TOPIC_URI, 678 691 title: 'Short Title', 679 692 content: longContent, ··· 713 726 714 727 await service.crossPostTopic({ 715 728 did: TEST_DID, 729 + handle: 'test.handle', 716 730 topicUri: TEST_TOPIC_URI, 717 731 title: 'URL Test', 718 732 content: 'Content.', ··· 727 741 ] 728 742 const embed = record.embed as Record<string, unknown> 729 743 const external = embed.external as Record<string, unknown> 730 - expect(external.uri).toBe(`${TEST_PUBLIC_URL}/topics/abc123`) 744 + expect(external.uri).toBe(`${TEST_PUBLIC_URL}/test.handle/abc123`) 731 745 }) 732 746 733 747 it('does not generate OG image for Frontpage-only cross-posts', async () => { ··· 751 765 752 766 await service.crossPostTopic({ 753 767 did: TEST_DID, 768 + handle: 'test.handle', 754 769 topicUri: TEST_TOPIC_URI, 755 770 title: 'FP Only', 756 771 content: 'No OG needed.',