Barazo AppView backend barazo.forum

feat: unify onboarding fields with source column (#124)

* feat(config): add HOSTING_MODE env var

Adds HOSTING_MODE with 'saas' | 'selfhosted' options, defaulting to
'selfhosted'. SaaS mode will restrict platform field modifications;
selfhosted gives full admin control over all onboarding fields.

Part of: barazo-forum/barazo-workspace#71

* feat(schema): add source column to community_onboarding_fields

Adds 'source' column with values 'platform' | 'admin' (default: 'admin').
Platform fields are seeded by Barazo; admin fields are created via the UI.
Extensible for future 'plugin' source.

Part of: barazo-forum/barazo-workspace#71

* feat(migration): backfill platform age field and user responses

Seeds platform:age_confirmation field for existing initialized communities.
Backfills user_onboarding_responses from user_preferences.declared_age so
users who already declared their age aren't re-prompted.

Part of: barazo-forum/barazo-workspace#71

* feat(setup): seed platform onboarding fields during initialization

After community initialization, seeds platform:age_confirmation field
with source='platform' and sortOrder=-1. Uses onConflictDoNothing for
idempotent re-runs.

Part of: barazo-forum/barazo-workspace#71

* feat(routes): add source field to admin onboarding endpoints with SaaS guards

- GET /api/admin/onboarding-fields now returns { fields, hostingMode }
- Each field includes source ('platform' | 'admin') in serialization
- PUT and DELETE reject platform field modifications in SaaS mode (403)
- Selfhosted mode allows full control over all fields

Part of: barazo-forum/barazo-workspace#71

* refactor(routes): remove virtual field injection from user endpoints

All onboarding fields (platform + admin) now come from the database.
Removed SYSTEM_AGE_FIELD_ID constant, virtual age field injection in
GET /api/onboarding/status, and system age submission separation in
POST /api/onboarding/submit. All fields validated and stored uniformly.

Part of: barazo-forum/barazo-workspace#71

* refactor(gate): simplify onboarding completeness check

Removed virtual system age field injection and user_preferences lookup.
The function now simply checks: are all mandatory DB fields answered?
Platform and admin fields are treated uniformly.

Part of: barazo-forum/barazo-workspace#71

* fix(migration): renumber migrations after rebase on main

Renumber source column migration from 0001 to 0002 and backfill
from 0002 to 0003, since main now has 0001_add_favicon_url.

authored by

Guido X Jansen and committed by
GitHub
7bca547a ab5ae9ec

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