Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: global labels

Hugo 102c6f26 968ea4a4

+1667 -43
+2
drizzle/0005_volatile_mystique.sql
··· 1 + ALTER TABLE `feature_requests` ADD `label_tid` text;--> statement-breakpoint 2 + ALTER TABLE `kanban_tasks` ADD `label_tid` text;
+1401
drizzle/meta/0005_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "f024260b-c00f-40d3-981d-89f2c8962fe8", 5 + "prevId": "36c47020-9349-4833-8827-8c5bde6e1b17", 6 + "tables": { 7 + "oauth_sessions": { 8 + "name": "oauth_sessions", 9 + "columns": { 10 + "key": { 11 + "name": "key", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "session": { 18 + "name": "session", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "created_at": { 25 + "name": "created_at", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false, 30 + "default": "(datetime('now'))" 31 + }, 32 + "updated_at": { 33 + "name": "updated_at", 34 + "type": "text", 35 + "primaryKey": false, 36 + "notNull": true, 37 + "autoincrement": false, 38 + "default": "(datetime('now'))" 39 + } 40 + }, 41 + "indexes": {}, 42 + "foreignKeys": {}, 43 + "compositePrimaryKeys": {}, 44 + "uniqueConstraints": {}, 45 + "checkConstraints": {} 46 + }, 47 + "oauth_states": { 48 + "name": "oauth_states", 49 + "columns": { 50 + "key": { 51 + "name": "key", 52 + "type": "text", 53 + "primaryKey": true, 54 + "notNull": true, 55 + "autoincrement": false 56 + }, 57 + "state": { 58 + "name": "state", 59 + "type": "text", 60 + "primaryKey": false, 61 + "notNull": true, 62 + "autoincrement": false 63 + }, 64 + "created_at": { 65 + "name": "created_at", 66 + "type": "text", 67 + "primaryKey": false, 68 + "notNull": true, 69 + "autoincrement": false, 70 + "default": "(datetime('now'))" 71 + } 72 + }, 73 + "indexes": {}, 74 + "foreignKeys": {}, 75 + "compositePrimaryKeys": {}, 76 + "uniqueConstraints": {}, 77 + "checkConstraints": {} 78 + }, 79 + "indexer_cursor": { 80 + "name": "indexer_cursor", 81 + "columns": { 82 + "id": { 83 + "name": "id", 84 + "type": "text", 85 + "primaryKey": true, 86 + "notNull": true, 87 + "autoincrement": false, 88 + "default": "'jetstream'" 89 + }, 90 + "cursor": { 91 + "name": "cursor", 92 + "type": "integer", 93 + "primaryKey": false, 94 + "notNull": true, 95 + "autoincrement": false 96 + }, 97 + "updated_at": { 98 + "name": "updated_at", 99 + "type": "text", 100 + "primaryKey": false, 101 + "notNull": true, 102 + "autoincrement": false, 103 + "default": "(datetime('now'))" 104 + } 105 + }, 106 + "indexes": {}, 107 + "foreignKeys": {}, 108 + "compositePrimaryKeys": {}, 109 + "uniqueConstraints": {}, 110 + "checkConstraints": {} 111 + }, 112 + "sphere_entry_counter": { 113 + "name": "sphere_entry_counter", 114 + "columns": { 115 + "sphere_id": { 116 + "name": "sphere_id", 117 + "type": "text", 118 + "primaryKey": true, 119 + "notNull": true, 120 + "autoincrement": false 121 + }, 122 + "last_number": { 123 + "name": "last_number", 124 + "type": "integer", 125 + "primaryKey": false, 126 + "notNull": true, 127 + "autoincrement": false, 128 + "default": 0 129 + } 130 + }, 131 + "indexes": {}, 132 + "foreignKeys": { 133 + "sphere_entry_counter_sphere_id_spheres_id_fk": { 134 + "name": "sphere_entry_counter_sphere_id_spheres_id_fk", 135 + "tableFrom": "sphere_entry_counter", 136 + "tableTo": "spheres", 137 + "columnsFrom": ["sphere_id"], 138 + "columnsTo": ["id"], 139 + "onDelete": "no action", 140 + "onUpdate": "no action" 141 + } 142 + }, 143 + "compositePrimaryKeys": {}, 144 + "uniqueConstraints": {}, 145 + "checkConstraints": {} 146 + }, 147 + "entity_labels": { 148 + "name": "entity_labels", 149 + "columns": { 150 + "label_id": { 151 + "name": "label_id", 152 + "type": "text", 153 + "primaryKey": false, 154 + "notNull": true, 155 + "autoincrement": false 156 + }, 157 + "entity_id": { 158 + "name": "entity_id", 159 + "type": "text", 160 + "primaryKey": false, 161 + "notNull": true, 162 + "autoincrement": false 163 + }, 164 + "entity_type": { 165 + "name": "entity_type", 166 + "type": "text", 167 + "primaryKey": false, 168 + "notNull": true, 169 + "autoincrement": false 170 + } 171 + }, 172 + "indexes": { 173 + "idx_entity_labels_entity": { 174 + "name": "idx_entity_labels_entity", 175 + "columns": ["entity_id", "entity_type"], 176 + "isUnique": false 177 + } 178 + }, 179 + "foreignKeys": { 180 + "entity_labels_label_id_sphere_labels_id_fk": { 181 + "name": "entity_labels_label_id_sphere_labels_id_fk", 182 + "tableFrom": "entity_labels", 183 + "tableTo": "sphere_labels", 184 + "columnsFrom": ["label_id"], 185 + "columnsTo": ["id"], 186 + "onDelete": "cascade", 187 + "onUpdate": "no action" 188 + } 189 + }, 190 + "compositePrimaryKeys": { 191 + "entity_labels_label_id_entity_id_pk": { 192 + "columns": ["label_id", "entity_id"], 193 + "name": "entity_labels_label_id_entity_id_pk" 194 + } 195 + }, 196 + "uniqueConstraints": {}, 197 + "checkConstraints": {} 198 + }, 199 + "sphere_labels": { 200 + "name": "sphere_labels", 201 + "columns": { 202 + "id": { 203 + "name": "id", 204 + "type": "text", 205 + "primaryKey": true, 206 + "notNull": true, 207 + "autoincrement": false 208 + }, 209 + "sphere_id": { 210 + "name": "sphere_id", 211 + "type": "text", 212 + "primaryKey": false, 213 + "notNull": true, 214 + "autoincrement": false 215 + }, 216 + "name": { 217 + "name": "name", 218 + "type": "text", 219 + "primaryKey": false, 220 + "notNull": true, 221 + "autoincrement": false 222 + }, 223 + "description": { 224 + "name": "description", 225 + "type": "text", 226 + "primaryKey": false, 227 + "notNull": false, 228 + "autoincrement": false 229 + }, 230 + "color": { 231 + "name": "color", 232 + "type": "text", 233 + "primaryKey": false, 234 + "notNull": true, 235 + "autoincrement": false 236 + }, 237 + "position": { 238 + "name": "position", 239 + "type": "integer", 240 + "primaryKey": false, 241 + "notNull": true, 242 + "autoincrement": false, 243 + "default": 0 244 + }, 245 + "created_at": { 246 + "name": "created_at", 247 + "type": "text", 248 + "primaryKey": false, 249 + "notNull": true, 250 + "autoincrement": false, 251 + "default": "(datetime('now'))" 252 + } 253 + }, 254 + "indexes": { 255 + "idx_sphere_labels_sphere_name": { 256 + "name": "idx_sphere_labels_sphere_name", 257 + "columns": ["sphere_id", "name"], 258 + "isUnique": true 259 + }, 260 + "idx_sphere_labels_sphere_position": { 261 + "name": "idx_sphere_labels_sphere_position", 262 + "columns": ["sphere_id", "position"], 263 + "isUnique": false 264 + } 265 + }, 266 + "foreignKeys": { 267 + "sphere_labels_sphere_id_spheres_id_fk": { 268 + "name": "sphere_labels_sphere_id_spheres_id_fk", 269 + "tableFrom": "sphere_labels", 270 + "tableTo": "spheres", 271 + "columnsFrom": ["sphere_id"], 272 + "columnsTo": ["id"], 273 + "onDelete": "no action", 274 + "onUpdate": "no action" 275 + } 276 + }, 277 + "compositePrimaryKeys": {}, 278 + "uniqueConstraints": {}, 279 + "checkConstraints": {} 280 + }, 281 + "sphere_members": { 282 + "name": "sphere_members", 283 + "columns": { 284 + "sphere_id": { 285 + "name": "sphere_id", 286 + "type": "text", 287 + "primaryKey": false, 288 + "notNull": true, 289 + "autoincrement": false 290 + }, 291 + "did": { 292 + "name": "did", 293 + "type": "text", 294 + "primaryKey": false, 295 + "notNull": true, 296 + "autoincrement": false 297 + }, 298 + "role": { 299 + "name": "role", 300 + "type": "text", 301 + "primaryKey": false, 302 + "notNull": true, 303 + "autoincrement": false, 304 + "default": "'member'" 305 + }, 306 + "status": { 307 + "name": "status", 308 + "type": "text", 309 + "primaryKey": false, 310 + "notNull": true, 311 + "autoincrement": false, 312 + "default": "'invited'" 313 + }, 314 + "invited_by": { 315 + "name": "invited_by", 316 + "type": "text", 317 + "primaryKey": false, 318 + "notNull": false, 319 + "autoincrement": false 320 + }, 321 + "pds_uri": { 322 + "name": "pds_uri", 323 + "type": "text", 324 + "primaryKey": false, 325 + "notNull": false, 326 + "autoincrement": false 327 + }, 328 + "approval_pds_uri": { 329 + "name": "approval_pds_uri", 330 + "type": "text", 331 + "primaryKey": false, 332 + "notNull": false, 333 + "autoincrement": false 334 + }, 335 + "created_at": { 336 + "name": "created_at", 337 + "type": "text", 338 + "primaryKey": false, 339 + "notNull": true, 340 + "autoincrement": false, 341 + "default": "(datetime('now'))" 342 + } 343 + }, 344 + "indexes": { 345 + "idx_sphere_members_did": { 346 + "name": "idx_sphere_members_did", 347 + "columns": ["did"], 348 + "isUnique": false 349 + } 350 + }, 351 + "foreignKeys": { 352 + "sphere_members_sphere_id_spheres_id_fk": { 353 + "name": "sphere_members_sphere_id_spheres_id_fk", 354 + "tableFrom": "sphere_members", 355 + "tableTo": "spheres", 356 + "columnsFrom": ["sphere_id"], 357 + "columnsTo": ["id"], 358 + "onDelete": "no action", 359 + "onUpdate": "no action" 360 + } 361 + }, 362 + "compositePrimaryKeys": { 363 + "sphere_members_sphere_id_did_pk": { 364 + "columns": ["sphere_id", "did"], 365 + "name": "sphere_members_sphere_id_did_pk" 366 + } 367 + }, 368 + "uniqueConstraints": {}, 369 + "checkConstraints": {} 370 + }, 371 + "sphere_modules": { 372 + "name": "sphere_modules", 373 + "columns": { 374 + "sphere_id": { 375 + "name": "sphere_id", 376 + "type": "text", 377 + "primaryKey": false, 378 + "notNull": true, 379 + "autoincrement": false 380 + }, 381 + "module_name": { 382 + "name": "module_name", 383 + "type": "text", 384 + "primaryKey": false, 385 + "notNull": true, 386 + "autoincrement": false 387 + }, 388 + "enabled_at": { 389 + "name": "enabled_at", 390 + "type": "text", 391 + "primaryKey": false, 392 + "notNull": true, 393 + "autoincrement": false, 394 + "default": "(datetime('now'))" 395 + } 396 + }, 397 + "indexes": {}, 398 + "foreignKeys": { 399 + "sphere_modules_sphere_id_spheres_id_fk": { 400 + "name": "sphere_modules_sphere_id_spheres_id_fk", 401 + "tableFrom": "sphere_modules", 402 + "tableTo": "spheres", 403 + "columnsFrom": ["sphere_id"], 404 + "columnsTo": ["id"], 405 + "onDelete": "no action", 406 + "onUpdate": "no action" 407 + } 408 + }, 409 + "compositePrimaryKeys": { 410 + "sphere_modules_sphere_id_module_name_pk": { 411 + "columns": ["sphere_id", "module_name"], 412 + "name": "sphere_modules_sphere_id_module_name_pk" 413 + } 414 + }, 415 + "uniqueConstraints": {}, 416 + "checkConstraints": {} 417 + }, 418 + "sphere_permissions": { 419 + "name": "sphere_permissions", 420 + "columns": { 421 + "sphere_id": { 422 + "name": "sphere_id", 423 + "type": "text", 424 + "primaryKey": false, 425 + "notNull": true, 426 + "autoincrement": false 427 + }, 428 + "action_key": { 429 + "name": "action_key", 430 + "type": "text", 431 + "primaryKey": false, 432 + "notNull": true, 433 + "autoincrement": false 434 + }, 435 + "min_role": { 436 + "name": "min_role", 437 + "type": "text", 438 + "primaryKey": false, 439 + "notNull": true, 440 + "autoincrement": false 441 + }, 442 + "updated_at": { 443 + "name": "updated_at", 444 + "type": "text", 445 + "primaryKey": false, 446 + "notNull": true, 447 + "autoincrement": false, 448 + "default": "(datetime('now'))" 449 + } 450 + }, 451 + "indexes": {}, 452 + "foreignKeys": { 453 + "sphere_permissions_sphere_id_spheres_id_fk": { 454 + "name": "sphere_permissions_sphere_id_spheres_id_fk", 455 + "tableFrom": "sphere_permissions", 456 + "tableTo": "spheres", 457 + "columnsFrom": ["sphere_id"], 458 + "columnsTo": ["id"], 459 + "onDelete": "no action", 460 + "onUpdate": "no action" 461 + } 462 + }, 463 + "compositePrimaryKeys": { 464 + "sphere_permissions_sphere_id_action_key_pk": { 465 + "columns": ["sphere_id", "action_key"], 466 + "name": "sphere_permissions_sphere_id_action_key_pk" 467 + } 468 + }, 469 + "uniqueConstraints": {}, 470 + "checkConstraints": {} 471 + }, 472 + "spheres": { 473 + "name": "spheres", 474 + "columns": { 475 + "id": { 476 + "name": "id", 477 + "type": "text", 478 + "primaryKey": true, 479 + "notNull": true, 480 + "autoincrement": false 481 + }, 482 + "handle": { 483 + "name": "handle", 484 + "type": "text", 485 + "primaryKey": false, 486 + "notNull": true, 487 + "autoincrement": false 488 + }, 489 + "name": { 490 + "name": "name", 491 + "type": "text", 492 + "primaryKey": false, 493 + "notNull": true, 494 + "autoincrement": false 495 + }, 496 + "description": { 497 + "name": "description", 498 + "type": "text", 499 + "primaryKey": false, 500 + "notNull": false, 501 + "autoincrement": false 502 + }, 503 + "visibility": { 504 + "name": "visibility", 505 + "type": "text", 506 + "primaryKey": false, 507 + "notNull": true, 508 + "autoincrement": false, 509 + "default": "'public'" 510 + }, 511 + "owner_did": { 512 + "name": "owner_did", 513 + "type": "text", 514 + "primaryKey": false, 515 + "notNull": true, 516 + "autoincrement": false 517 + }, 518 + "pds_uri": { 519 + "name": "pds_uri", 520 + "type": "text", 521 + "primaryKey": false, 522 + "notNull": false, 523 + "autoincrement": false 524 + }, 525 + "created_at": { 526 + "name": "created_at", 527 + "type": "text", 528 + "primaryKey": false, 529 + "notNull": true, 530 + "autoincrement": false, 531 + "default": "(datetime('now'))" 532 + }, 533 + "updated_at": { 534 + "name": "updated_at", 535 + "type": "text", 536 + "primaryKey": false, 537 + "notNull": true, 538 + "autoincrement": false, 539 + "default": "(datetime('now'))" 540 + } 541 + }, 542 + "indexes": { 543 + "spheres_handle_unique": { 544 + "name": "spheres_handle_unique", 545 + "columns": ["handle"], 546 + "isUnique": true 547 + } 548 + }, 549 + "foreignKeys": {}, 550 + "compositePrimaryKeys": {}, 551 + "uniqueConstraints": {}, 552 + "checkConstraints": {} 553 + }, 554 + "feature_request_comment_votes": { 555 + "name": "feature_request_comment_votes", 556 + "columns": { 557 + "comment_id": { 558 + "name": "comment_id", 559 + "type": "text", 560 + "primaryKey": false, 561 + "notNull": true, 562 + "autoincrement": false 563 + }, 564 + "author_did": { 565 + "name": "author_did", 566 + "type": "text", 567 + "primaryKey": false, 568 + "notNull": true, 569 + "autoincrement": false 570 + }, 571 + "pds_uri": { 572 + "name": "pds_uri", 573 + "type": "text", 574 + "primaryKey": false, 575 + "notNull": false, 576 + "autoincrement": false 577 + }, 578 + "created_at": { 579 + "name": "created_at", 580 + "type": "text", 581 + "primaryKey": false, 582 + "notNull": true, 583 + "autoincrement": false, 584 + "default": "(datetime('now'))" 585 + } 586 + }, 587 + "indexes": { 588 + "idx_feature_request_comment_votes_comment": { 589 + "name": "idx_feature_request_comment_votes_comment", 590 + "columns": ["comment_id"], 591 + "isUnique": false 592 + } 593 + }, 594 + "foreignKeys": { 595 + "feature_request_comment_votes_comment_id_feature_request_comments_id_fk": { 596 + "name": "feature_request_comment_votes_comment_id_feature_request_comments_id_fk", 597 + "tableFrom": "feature_request_comment_votes", 598 + "tableTo": "feature_request_comments", 599 + "columnsFrom": ["comment_id"], 600 + "columnsTo": ["id"], 601 + "onDelete": "no action", 602 + "onUpdate": "no action" 603 + } 604 + }, 605 + "compositePrimaryKeys": { 606 + "feature_request_comment_votes_comment_id_author_did_pk": { 607 + "columns": ["comment_id", "author_did"], 608 + "name": "feature_request_comment_votes_comment_id_author_did_pk" 609 + } 610 + }, 611 + "uniqueConstraints": {}, 612 + "checkConstraints": {} 613 + }, 614 + "feature_request_comments": { 615 + "name": "feature_request_comments", 616 + "columns": { 617 + "id": { 618 + "name": "id", 619 + "type": "text", 620 + "primaryKey": true, 621 + "notNull": true, 622 + "autoincrement": false 623 + }, 624 + "request_id": { 625 + "name": "request_id", 626 + "type": "text", 627 + "primaryKey": false, 628 + "notNull": true, 629 + "autoincrement": false 630 + }, 631 + "author_did": { 632 + "name": "author_did", 633 + "type": "text", 634 + "primaryKey": false, 635 + "notNull": true, 636 + "autoincrement": false 637 + }, 638 + "content": { 639 + "name": "content", 640 + "type": "text", 641 + "primaryKey": false, 642 + "notNull": true, 643 + "autoincrement": false 644 + }, 645 + "pds_uri": { 646 + "name": "pds_uri", 647 + "type": "text", 648 + "primaryKey": false, 649 + "notNull": false, 650 + "autoincrement": false 651 + }, 652 + "updated_at": { 653 + "name": "updated_at", 654 + "type": "text", 655 + "primaryKey": false, 656 + "notNull": true, 657 + "autoincrement": false, 658 + "default": "(datetime('now'))" 659 + }, 660 + "hidden_at": { 661 + "name": "hidden_at", 662 + "type": "text", 663 + "primaryKey": false, 664 + "notNull": false, 665 + "autoincrement": false 666 + }, 667 + "moderated_by": { 668 + "name": "moderated_by", 669 + "type": "text", 670 + "primaryKey": false, 671 + "notNull": false, 672 + "autoincrement": false 673 + } 674 + }, 675 + "indexes": { 676 + "idx_feature_request_comments_request": { 677 + "name": "idx_feature_request_comments_request", 678 + "columns": ["request_id"], 679 + "isUnique": false 680 + }, 681 + "idx_feature_request_comments_author_request": { 682 + "name": "idx_feature_request_comments_author_request", 683 + "columns": ["author_did", "request_id"], 684 + "isUnique": false 685 + } 686 + }, 687 + "foreignKeys": { 688 + "feature_request_comments_request_id_feature_requests_id_fk": { 689 + "name": "feature_request_comments_request_id_feature_requests_id_fk", 690 + "tableFrom": "feature_request_comments", 691 + "tableTo": "feature_requests", 692 + "columnsFrom": ["request_id"], 693 + "columnsTo": ["id"], 694 + "onDelete": "no action", 695 + "onUpdate": "no action" 696 + } 697 + }, 698 + "compositePrimaryKeys": {}, 699 + "uniqueConstraints": {}, 700 + "checkConstraints": {} 701 + }, 702 + "feature_request_statuses": { 703 + "name": "feature_request_statuses", 704 + "columns": { 705 + "id": { 706 + "name": "id", 707 + "type": "text", 708 + "primaryKey": true, 709 + "notNull": true, 710 + "autoincrement": false 711 + }, 712 + "request_id": { 713 + "name": "request_id", 714 + "type": "text", 715 + "primaryKey": false, 716 + "notNull": true, 717 + "autoincrement": false 718 + }, 719 + "author_did": { 720 + "name": "author_did", 721 + "type": "text", 722 + "primaryKey": false, 723 + "notNull": true, 724 + "autoincrement": false 725 + }, 726 + "status": { 727 + "name": "status", 728 + "type": "text", 729 + "primaryKey": false, 730 + "notNull": true, 731 + "autoincrement": false 732 + }, 733 + "pds_uri": { 734 + "name": "pds_uri", 735 + "type": "text", 736 + "primaryKey": false, 737 + "notNull": false, 738 + "autoincrement": false 739 + } 740 + }, 741 + "indexes": { 742 + "idx_feature_request_statuses_request": { 743 + "name": "idx_feature_request_statuses_request", 744 + "columns": ["request_id"], 745 + "isUnique": false 746 + } 747 + }, 748 + "foreignKeys": { 749 + "feature_request_statuses_request_id_feature_requests_id_fk": { 750 + "name": "feature_request_statuses_request_id_feature_requests_id_fk", 751 + "tableFrom": "feature_request_statuses", 752 + "tableTo": "feature_requests", 753 + "columnsFrom": ["request_id"], 754 + "columnsTo": ["id"], 755 + "onDelete": "no action", 756 + "onUpdate": "no action" 757 + } 758 + }, 759 + "compositePrimaryKeys": {}, 760 + "uniqueConstraints": {}, 761 + "checkConstraints": {} 762 + }, 763 + "feature_request_votes": { 764 + "name": "feature_request_votes", 765 + "columns": { 766 + "request_id": { 767 + "name": "request_id", 768 + "type": "text", 769 + "primaryKey": false, 770 + "notNull": true, 771 + "autoincrement": false 772 + }, 773 + "author_did": { 774 + "name": "author_did", 775 + "type": "text", 776 + "primaryKey": false, 777 + "notNull": true, 778 + "autoincrement": false 779 + }, 780 + "pds_uri": { 781 + "name": "pds_uri", 782 + "type": "text", 783 + "primaryKey": false, 784 + "notNull": false, 785 + "autoincrement": false 786 + }, 787 + "created_at": { 788 + "name": "created_at", 789 + "type": "text", 790 + "primaryKey": false, 791 + "notNull": true, 792 + "autoincrement": false, 793 + "default": "(datetime('now'))" 794 + } 795 + }, 796 + "indexes": { 797 + "idx_feature_request_votes_request": { 798 + "name": "idx_feature_request_votes_request", 799 + "columns": ["request_id"], 800 + "isUnique": false 801 + } 802 + }, 803 + "foreignKeys": { 804 + "feature_request_votes_request_id_feature_requests_id_fk": { 805 + "name": "feature_request_votes_request_id_feature_requests_id_fk", 806 + "tableFrom": "feature_request_votes", 807 + "tableTo": "feature_requests", 808 + "columnsFrom": ["request_id"], 809 + "columnsTo": ["id"], 810 + "onDelete": "no action", 811 + "onUpdate": "no action" 812 + } 813 + }, 814 + "compositePrimaryKeys": { 815 + "feature_request_votes_request_id_author_did_pk": { 816 + "columns": ["request_id", "author_did"], 817 + "name": "feature_request_votes_request_id_author_did_pk" 818 + } 819 + }, 820 + "uniqueConstraints": {}, 821 + "checkConstraints": {} 822 + }, 823 + "feature_requests": { 824 + "name": "feature_requests", 825 + "columns": { 826 + "id": { 827 + "name": "id", 828 + "type": "text", 829 + "primaryKey": true, 830 + "notNull": true, 831 + "autoincrement": false 832 + }, 833 + "sphere_id": { 834 + "name": "sphere_id", 835 + "type": "text", 836 + "primaryKey": false, 837 + "notNull": true, 838 + "autoincrement": false 839 + }, 840 + "number": { 841 + "name": "number", 842 + "type": "integer", 843 + "primaryKey": false, 844 + "notNull": true, 845 + "autoincrement": false 846 + }, 847 + "author_did": { 848 + "name": "author_did", 849 + "type": "text", 850 + "primaryKey": false, 851 + "notNull": true, 852 + "autoincrement": false 853 + }, 854 + "title": { 855 + "name": "title", 856 + "type": "text", 857 + "primaryKey": false, 858 + "notNull": true, 859 + "autoincrement": false 860 + }, 861 + "description": { 862 + "name": "description", 863 + "type": "text", 864 + "primaryKey": false, 865 + "notNull": true, 866 + "autoincrement": false 867 + }, 868 + "category": { 869 + "name": "category", 870 + "type": "text", 871 + "primaryKey": false, 872 + "notNull": true, 873 + "autoincrement": false, 874 + "default": "'general'" 875 + }, 876 + "status": { 877 + "name": "status", 878 + "type": "text", 879 + "primaryKey": false, 880 + "notNull": true, 881 + "autoincrement": false, 882 + "default": "'requested'" 883 + }, 884 + "duplicate_of_id": { 885 + "name": "duplicate_of_id", 886 + "type": "text", 887 + "primaryKey": false, 888 + "notNull": false, 889 + "autoincrement": false 890 + }, 891 + "pds_uri": { 892 + "name": "pds_uri", 893 + "type": "text", 894 + "primaryKey": false, 895 + "notNull": false, 896 + "autoincrement": false 897 + }, 898 + "hidden_at": { 899 + "name": "hidden_at", 900 + "type": "text", 901 + "primaryKey": false, 902 + "notNull": false, 903 + "autoincrement": false 904 + }, 905 + "moderated_by": { 906 + "name": "moderated_by", 907 + "type": "text", 908 + "primaryKey": false, 909 + "notNull": false, 910 + "autoincrement": false 911 + }, 912 + "label_tid": { 913 + "name": "label_tid", 914 + "type": "text", 915 + "primaryKey": false, 916 + "notNull": false, 917 + "autoincrement": false 918 + }, 919 + "updated_at": { 920 + "name": "updated_at", 921 + "type": "text", 922 + "primaryKey": false, 923 + "notNull": true, 924 + "autoincrement": false, 925 + "default": "(datetime('now'))" 926 + } 927 + }, 928 + "indexes": { 929 + "idx_feature_requests_sphere_number": { 930 + "name": "idx_feature_requests_sphere_number", 931 + "columns": ["sphere_id", "number"], 932 + "isUnique": true 933 + }, 934 + "idx_feature_requests_sphere": { 935 + "name": "idx_feature_requests_sphere", 936 + "columns": ["sphere_id"], 937 + "isUnique": false 938 + }, 939 + "idx_feature_requests_status": { 940 + "name": "idx_feature_requests_status", 941 + "columns": ["status"], 942 + "isUnique": false 943 + }, 944 + "idx_feature_requests_category": { 945 + "name": "idx_feature_requests_category", 946 + "columns": ["category"], 947 + "isUnique": false 948 + } 949 + }, 950 + "foreignKeys": { 951 + "feature_requests_sphere_id_spheres_id_fk": { 952 + "name": "feature_requests_sphere_id_spheres_id_fk", 953 + "tableFrom": "feature_requests", 954 + "tableTo": "spheres", 955 + "columnsFrom": ["sphere_id"], 956 + "columnsTo": ["id"], 957 + "onDelete": "no action", 958 + "onUpdate": "no action" 959 + } 960 + }, 961 + "compositePrimaryKeys": {}, 962 + "uniqueConstraints": {}, 963 + "checkConstraints": {} 964 + }, 965 + "feed_posts": { 966 + "name": "feed_posts", 967 + "columns": { 968 + "id": { 969 + "name": "id", 970 + "type": "text", 971 + "primaryKey": true, 972 + "notNull": true, 973 + "autoincrement": false 974 + }, 975 + "author_did": { 976 + "name": "author_did", 977 + "type": "text", 978 + "primaryKey": false, 979 + "notNull": true, 980 + "autoincrement": false 981 + }, 982 + "content": { 983 + "name": "content", 984 + "type": "text", 985 + "primaryKey": false, 986 + "notNull": true, 987 + "autoincrement": false 988 + }, 989 + "parent_id": { 990 + "name": "parent_id", 991 + "type": "text", 992 + "primaryKey": false, 993 + "notNull": false, 994 + "autoincrement": false 995 + }, 996 + "pds_uri": { 997 + "name": "pds_uri", 998 + "type": "text", 999 + "primaryKey": false, 1000 + "notNull": false, 1001 + "autoincrement": false 1002 + }, 1003 + "updated_at": { 1004 + "name": "updated_at", 1005 + "type": "text", 1006 + "primaryKey": false, 1007 + "notNull": true, 1008 + "autoincrement": false, 1009 + "default": "(datetime('now'))" 1010 + } 1011 + }, 1012 + "indexes": { 1013 + "idx_feed_posts_parent": { 1014 + "name": "idx_feed_posts_parent", 1015 + "columns": ["parent_id"], 1016 + "isUnique": false 1017 + } 1018 + }, 1019 + "foreignKeys": {}, 1020 + "compositePrimaryKeys": {}, 1021 + "uniqueConstraints": {}, 1022 + "checkConstraints": {} 1023 + }, 1024 + "kanban_columns": { 1025 + "name": "kanban_columns", 1026 + "columns": { 1027 + "id": { 1028 + "name": "id", 1029 + "type": "text", 1030 + "primaryKey": true, 1031 + "notNull": true, 1032 + "autoincrement": false 1033 + }, 1034 + "sphere_id": { 1035 + "name": "sphere_id", 1036 + "type": "text", 1037 + "primaryKey": false, 1038 + "notNull": true, 1039 + "autoincrement": false 1040 + }, 1041 + "slug": { 1042 + "name": "slug", 1043 + "type": "text", 1044 + "primaryKey": false, 1045 + "notNull": true, 1046 + "autoincrement": false 1047 + }, 1048 + "label": { 1049 + "name": "label", 1050 + "type": "text", 1051 + "primaryKey": false, 1052 + "notNull": true, 1053 + "autoincrement": false 1054 + }, 1055 + "position": { 1056 + "name": "position", 1057 + "type": "integer", 1058 + "primaryKey": false, 1059 + "notNull": true, 1060 + "autoincrement": false 1061 + }, 1062 + "created_at": { 1063 + "name": "created_at", 1064 + "type": "text", 1065 + "primaryKey": false, 1066 + "notNull": true, 1067 + "autoincrement": false, 1068 + "default": "(datetime('now'))" 1069 + } 1070 + }, 1071 + "indexes": { 1072 + "idx_kanban_columns_sphere_slug": { 1073 + "name": "idx_kanban_columns_sphere_slug", 1074 + "columns": ["sphere_id", "slug"], 1075 + "isUnique": true 1076 + }, 1077 + "idx_kanban_columns_sphere_position": { 1078 + "name": "idx_kanban_columns_sphere_position", 1079 + "columns": ["sphere_id", "position"], 1080 + "isUnique": false 1081 + } 1082 + }, 1083 + "foreignKeys": { 1084 + "kanban_columns_sphere_id_spheres_id_fk": { 1085 + "name": "kanban_columns_sphere_id_spheres_id_fk", 1086 + "tableFrom": "kanban_columns", 1087 + "tableTo": "spheres", 1088 + "columnsFrom": ["sphere_id"], 1089 + "columnsTo": ["id"], 1090 + "onDelete": "no action", 1091 + "onUpdate": "no action" 1092 + } 1093 + }, 1094 + "compositePrimaryKeys": {}, 1095 + "uniqueConstraints": {}, 1096 + "checkConstraints": {} 1097 + }, 1098 + "kanban_task_comments": { 1099 + "name": "kanban_task_comments", 1100 + "columns": { 1101 + "id": { 1102 + "name": "id", 1103 + "type": "text", 1104 + "primaryKey": true, 1105 + "notNull": true, 1106 + "autoincrement": false 1107 + }, 1108 + "task_id": { 1109 + "name": "task_id", 1110 + "type": "text", 1111 + "primaryKey": false, 1112 + "notNull": true, 1113 + "autoincrement": false 1114 + }, 1115 + "author_did": { 1116 + "name": "author_did", 1117 + "type": "text", 1118 + "primaryKey": false, 1119 + "notNull": true, 1120 + "autoincrement": false 1121 + }, 1122 + "content": { 1123 + "name": "content", 1124 + "type": "text", 1125 + "primaryKey": false, 1126 + "notNull": true, 1127 + "autoincrement": false 1128 + }, 1129 + "pds_uri": { 1130 + "name": "pds_uri", 1131 + "type": "text", 1132 + "primaryKey": false, 1133 + "notNull": false, 1134 + "autoincrement": false 1135 + }, 1136 + "updated_at": { 1137 + "name": "updated_at", 1138 + "type": "text", 1139 + "primaryKey": false, 1140 + "notNull": true, 1141 + "autoincrement": false, 1142 + "default": "(datetime('now'))" 1143 + }, 1144 + "hidden_at": { 1145 + "name": "hidden_at", 1146 + "type": "text", 1147 + "primaryKey": false, 1148 + "notNull": false, 1149 + "autoincrement": false 1150 + }, 1151 + "moderated_by": { 1152 + "name": "moderated_by", 1153 + "type": "text", 1154 + "primaryKey": false, 1155 + "notNull": false, 1156 + "autoincrement": false 1157 + } 1158 + }, 1159 + "indexes": { 1160 + "idx_kanban_task_comments_task": { 1161 + "name": "idx_kanban_task_comments_task", 1162 + "columns": ["task_id"], 1163 + "isUnique": false 1164 + }, 1165 + "idx_kanban_task_comments_author_task": { 1166 + "name": "idx_kanban_task_comments_author_task", 1167 + "columns": ["author_did", "task_id"], 1168 + "isUnique": false 1169 + } 1170 + }, 1171 + "foreignKeys": { 1172 + "kanban_task_comments_task_id_kanban_tasks_id_fk": { 1173 + "name": "kanban_task_comments_task_id_kanban_tasks_id_fk", 1174 + "tableFrom": "kanban_task_comments", 1175 + "tableTo": "kanban_tasks", 1176 + "columnsFrom": ["task_id"], 1177 + "columnsTo": ["id"], 1178 + "onDelete": "no action", 1179 + "onUpdate": "no action" 1180 + } 1181 + }, 1182 + "compositePrimaryKeys": {}, 1183 + "uniqueConstraints": {}, 1184 + "checkConstraints": {} 1185 + }, 1186 + "kanban_task_status_changes": { 1187 + "name": "kanban_task_status_changes", 1188 + "columns": { 1189 + "id": { 1190 + "name": "id", 1191 + "type": "text", 1192 + "primaryKey": true, 1193 + "notNull": true, 1194 + "autoincrement": false 1195 + }, 1196 + "task_id": { 1197 + "name": "task_id", 1198 + "type": "text", 1199 + "primaryKey": false, 1200 + "notNull": true, 1201 + "autoincrement": false 1202 + }, 1203 + "author_did": { 1204 + "name": "author_did", 1205 + "type": "text", 1206 + "primaryKey": false, 1207 + "notNull": true, 1208 + "autoincrement": false 1209 + }, 1210 + "status": { 1211 + "name": "status", 1212 + "type": "text", 1213 + "primaryKey": false, 1214 + "notNull": true, 1215 + "autoincrement": false 1216 + }, 1217 + "pds_uri": { 1218 + "name": "pds_uri", 1219 + "type": "text", 1220 + "primaryKey": false, 1221 + "notNull": false, 1222 + "autoincrement": false 1223 + } 1224 + }, 1225 + "indexes": { 1226 + "idx_kanban_task_status_changes_task": { 1227 + "name": "idx_kanban_task_status_changes_task", 1228 + "columns": ["task_id"], 1229 + "isUnique": false 1230 + } 1231 + }, 1232 + "foreignKeys": { 1233 + "kanban_task_status_changes_task_id_kanban_tasks_id_fk": { 1234 + "name": "kanban_task_status_changes_task_id_kanban_tasks_id_fk", 1235 + "tableFrom": "kanban_task_status_changes", 1236 + "tableTo": "kanban_tasks", 1237 + "columnsFrom": ["task_id"], 1238 + "columnsTo": ["id"], 1239 + "onDelete": "no action", 1240 + "onUpdate": "no action" 1241 + } 1242 + }, 1243 + "compositePrimaryKeys": {}, 1244 + "uniqueConstraints": {}, 1245 + "checkConstraints": {} 1246 + }, 1247 + "kanban_tasks": { 1248 + "name": "kanban_tasks", 1249 + "columns": { 1250 + "id": { 1251 + "name": "id", 1252 + "type": "text", 1253 + "primaryKey": true, 1254 + "notNull": true, 1255 + "autoincrement": false 1256 + }, 1257 + "sphere_id": { 1258 + "name": "sphere_id", 1259 + "type": "text", 1260 + "primaryKey": false, 1261 + "notNull": true, 1262 + "autoincrement": false 1263 + }, 1264 + "number": { 1265 + "name": "number", 1266 + "type": "integer", 1267 + "primaryKey": false, 1268 + "notNull": true, 1269 + "autoincrement": false 1270 + }, 1271 + "author_did": { 1272 + "name": "author_did", 1273 + "type": "text", 1274 + "primaryKey": false, 1275 + "notNull": true, 1276 + "autoincrement": false 1277 + }, 1278 + "title": { 1279 + "name": "title", 1280 + "type": "text", 1281 + "primaryKey": false, 1282 + "notNull": true, 1283 + "autoincrement": false 1284 + }, 1285 + "description": { 1286 + "name": "description", 1287 + "type": "text", 1288 + "primaryKey": false, 1289 + "notNull": true, 1290 + "autoincrement": false, 1291 + "default": "''" 1292 + }, 1293 + "status": { 1294 + "name": "status", 1295 + "type": "text", 1296 + "primaryKey": false, 1297 + "notNull": true, 1298 + "autoincrement": false, 1299 + "default": "'backlog'" 1300 + }, 1301 + "position": { 1302 + "name": "position", 1303 + "type": "integer", 1304 + "primaryKey": false, 1305 + "notNull": true, 1306 + "autoincrement": false, 1307 + "default": 0 1308 + }, 1309 + "assignee_did": { 1310 + "name": "assignee_did", 1311 + "type": "text", 1312 + "primaryKey": false, 1313 + "notNull": false, 1314 + "autoincrement": false 1315 + }, 1316 + "pds_uri": { 1317 + "name": "pds_uri", 1318 + "type": "text", 1319 + "primaryKey": false, 1320 + "notNull": false, 1321 + "autoincrement": false 1322 + }, 1323 + "hidden_at": { 1324 + "name": "hidden_at", 1325 + "type": "text", 1326 + "primaryKey": false, 1327 + "notNull": false, 1328 + "autoincrement": false 1329 + }, 1330 + "moderated_by": { 1331 + "name": "moderated_by", 1332 + "type": "text", 1333 + "primaryKey": false, 1334 + "notNull": false, 1335 + "autoincrement": false 1336 + }, 1337 + "label_tid": { 1338 + "name": "label_tid", 1339 + "type": "text", 1340 + "primaryKey": false, 1341 + "notNull": false, 1342 + "autoincrement": false 1343 + }, 1344 + "updated_at": { 1345 + "name": "updated_at", 1346 + "type": "text", 1347 + "primaryKey": false, 1348 + "notNull": true, 1349 + "autoincrement": false, 1350 + "default": "(datetime('now'))" 1351 + } 1352 + }, 1353 + "indexes": { 1354 + "idx_kanban_tasks_sphere_number": { 1355 + "name": "idx_kanban_tasks_sphere_number", 1356 + "columns": ["sphere_id", "number"], 1357 + "isUnique": true 1358 + }, 1359 + "idx_kanban_tasks_sphere": { 1360 + "name": "idx_kanban_tasks_sphere", 1361 + "columns": ["sphere_id"], 1362 + "isUnique": false 1363 + }, 1364 + "idx_kanban_tasks_status": { 1365 + "name": "idx_kanban_tasks_status", 1366 + "columns": ["status"], 1367 + "isUnique": false 1368 + }, 1369 + "idx_kanban_tasks_sphere_status_position": { 1370 + "name": "idx_kanban_tasks_sphere_status_position", 1371 + "columns": ["sphere_id", "status", "position"], 1372 + "isUnique": false 1373 + } 1374 + }, 1375 + "foreignKeys": { 1376 + "kanban_tasks_sphere_id_spheres_id_fk": { 1377 + "name": "kanban_tasks_sphere_id_spheres_id_fk", 1378 + "tableFrom": "kanban_tasks", 1379 + "tableTo": "spheres", 1380 + "columnsFrom": ["sphere_id"], 1381 + "columnsTo": ["id"], 1382 + "onDelete": "no action", 1383 + "onUpdate": "no action" 1384 + } 1385 + }, 1386 + "compositePrimaryKeys": {}, 1387 + "uniqueConstraints": {}, 1388 + "checkConstraints": {} 1389 + } 1390 + }, 1391 + "views": {}, 1392 + "enums": {}, 1393 + "_meta": { 1394 + "schemas": {}, 1395 + "tables": {}, 1396 + "columns": {} 1397 + }, 1398 + "internal": { 1399 + "indexes": {} 1400 + } 1401 + }
+7
drizzle/meta/_journal.json
··· 36 36 "when": 1776203212384, 37 37 "tag": "0004_melted_brother_voodoo", 38 38 "breakpoints": true 39 + }, 40 + { 41 + "idx": 5, 42 + "version": "6", 43 + "when": 1776250350625, 44 + "tag": "0005_volatile_mystique", 45 + "breakpoints": true 39 46 } 40 47 ] 41 48 }
+1 -3
packages/app/src/server.ts
··· 197 197 } 198 198 199 199 // For individual kanban tasks, also prefetch comments 200 - const taskData = pageData["kanban-task"] as 201 - | { task?: { id: string } } 202 - | undefined; 200 + const taskData = pageData["kanban-task"] as { task?: { id: string } } | undefined; 203 201 if (taskData?.task?.id) { 204 202 try { 205 203 const res = await app.request(
+4 -7
packages/app/src/vite-ssr-plugin.ts
··· 167 167 } 168 168 169 169 // For individual kanban tasks, also prefetch comments 170 - const taskData = pageData["kanban-task"] as 171 - | { task?: { id: string } } 172 - | undefined; 170 + const taskData = pageData["kanban-task"] as { task?: { id: string } } | undefined; 173 171 if (taskData?.task?.id) { 174 172 try { 175 - const res = await fetch( 176 - `${apiBase}/kanban/${taskData.task.id}/comments`, 177 - { headers: { cookie } }, 178 - ); 173 + const res = await fetch(`${apiBase}/kanban/${taskData.task.id}/comments`, { 174 + headers: { cookie }, 175 + }); 179 176 if (res.ok) pageData["kanban-task-comments"] = await res.json(); 180 177 } catch { 181 178 /* prefetch failed — client will fetch */
+8 -8
packages/core/src/db/migrate.ts
··· 4 4 5 5 const migrationsFolder = process.env.MIGRATIONS_PATH || "drizzle"; 6 6 7 + const categoryDefaults = [ 8 + { name: "General", slug: "general", color: "#6b7280", position: 0 }, 9 + { name: "Enhancement", slug: "enhancement", color: "#3b82f6", position: 1 }, 10 + { name: "Bug", slug: "bug", color: "#ef4444", position: 2 }, 11 + { name: "Integration", slug: "integration", color: "#8b5cf6", position: 3 }, 12 + { name: "UI / UX", slug: "ui-ux", color: "#f59e0b", position: 4 }, 13 + ] as const; 14 + 7 15 migrate(getDb(), { migrationsFolder }); 8 16 9 17 // Data migration: renumber feature requests and kanban tasks to use the ··· 88 96 db.run("PRAGMA foreign_keys = ON;"); 89 97 db.close(); 90 98 } 91 - 92 - const categoryDefaults = [ 93 - { name: "General", slug: "general", color: "#6b7280", position: 0 }, 94 - { name: "Enhancement", slug: "enhancement", color: "#3b82f6", position: 1 }, 95 - { name: "Bug", slug: "bug", color: "#ef4444", position: 2 }, 96 - { name: "Integration", slug: "integration", color: "#8b5cf6", position: 3 }, 97 - { name: "UI / UX", slug: "ui-ux", color: "#f59e0b", position: 4 }, 98 - ] as const; 99 99 100 100 function seedLabelsFromCategories() { 101 101 const dbPath = process.env.DATABASE_PATH || "exosphere.sqlite";
+8
packages/core/src/generated/lexicon-records.ts
··· 95 95 status: string; 96 96 } 97 97 98 + export interface LabelRecord { 99 + /** at-uri — AT URI of the labeled entity. */ 100 + subject: string; 101 + /** Label names assigned to the entity. Replaces any previous set. */ 102 + labels: string[]; 103 + } 104 + 98 105 export interface ModerationRecord { 99 106 /** at-uri — AT URI of the Sphere. */ 100 107 sphere: string; ··· 156 163 "site.exosphere.kanban.entry": KanbanEntryRecord; 157 164 "site.exosphere.kanban.permissions": KanbanPermissionsRecord; 158 165 "site.exosphere.kanban.status": KanbanStatusRecord; 166 + "site.exosphere.label": LabelRecord; 159 167 "site.exosphere.moderation": ModerationRecord; 160 168 "site.exosphere.sphere.member": SphereMemberRecord; 161 169 "site.exosphere.sphere.memberApproval": SphereMemberApprovalRecord;
+13 -3
packages/core/src/sphere/index.ts
··· 5 5 getPendingInvitations, 6 6 } from "./routes.ts"; 7 7 export { createCoreIndexer } from "./indexer.ts"; 8 - export { getActiveMemberRole, registerModerationHandler, findSphereByAtUri } from "./operations.ts"; 9 - export type { ModerationHandler } from "./operations.ts"; 8 + export { 9 + getActiveMemberRole, 10 + registerModerationHandler, 11 + registerLabelHandler, 12 + findSphereByAtUri, 13 + } from "./operations.ts"; 14 + export type { ModerationHandler, LabelHandler } from "./operations.ts"; 10 15 export { sphereContext } from "./middleware.ts"; 11 - export { getLabelsForSphere, getLabelsForEntities, setEntityLabels } from "./label-operations.ts"; 16 + export { 17 + getLabelsForSphere, 18 + getLabelsForEntities, 19 + setEntityLabels, 20 + resolveLabelIdsByName, 21 + } from "./label-operations.ts"; 12 22 export type { LabelInfo } from "./label-operations.ts"; 13 23 export { setEntityLabelsSchema } from "./schemas.ts";
+6
packages/core/src/sphere/indexer.ts
··· 7 7 deleteSphereMemberApproval, 8 8 deleteSphereMember, 9 9 indexModerationAction, 10 + indexLabelAssignment, 10 11 syncModulePermissionsFromRecord, 11 12 deleteModulePermissions, 12 13 } from "./operations.ts"; ··· 16 17 const MEMBER_COLLECTION = "site.exosphere.sphere.member"; 17 18 const APPROVAL_COLLECTION = "site.exosphere.sphere.memberApproval"; 18 19 const MODERATION_COLLECTION = "site.exosphere.moderation"; 20 + const LABEL_COLLECTION = "site.exosphere.label"; 19 21 20 22 /** Build the core indexer dynamically — includes permission collections from registered modules. */ 21 23 export function createCoreIndexer(): ModuleIndexer { ··· 31 33 MEMBER_COLLECTION, 32 34 APPROVAL_COLLECTION, 33 35 MODERATION_COLLECTION, 36 + LABEL_COLLECTION, 34 37 ]; 35 38 const allCollections = [...baseCollections, ...collectionToModule.keys()]; 36 39 ··· 63 66 break; 64 67 case MODERATION_COLLECTION: 65 68 indexModerationAction(did, record); 69 + break; 70 + case LABEL_COLLECTION: 71 + indexLabelAssignment(did, rkey, record); 66 72 break; 67 73 } 68 74 },
+11
packages/core/src/sphere/label-operations.ts
··· 93 93 94 94 export type LabelInfo = { id: string; name: string; color: string }; 95 95 96 + /** Resolve label names to local IDs for a sphere. Unknown names are silently skipped. */ 97 + export function resolveLabelIdsByName(sphereId: string, names: string[]): string[] { 98 + if (names.length === 0) return []; 99 + return getDb() 100 + .select({ id: sphereLabels.id }) 101 + .from(sphereLabels) 102 + .where(and(eq(sphereLabels.sphereId, sphereId), inArray(sphereLabels.name, names))) 103 + .all() 104 + .map((r) => r.id); 105 + } 106 + 96 107 /** Batch-fetch labels for multiple entities. Returns a Map keyed by entity ID. */ 97 108 export function getLabelsForEntities( 98 109 entityIds: string[],
+32
packages/core/src/sphere/operations.ts
··· 293 293 if (handler(subjectUri, did)) return; 294 294 } 295 295 } 296 + 297 + // ---- Labels ---- 298 + 299 + /** Callback that resolves a label assignment for an entity. 300 + * Returns true if this handler owns the subject collection. */ 301 + export type LabelHandler = ( 302 + subjectUri: string, 303 + labelNames: string[], 304 + actorDid: string, 305 + tid: string, 306 + ) => boolean; 307 + 308 + const labelHandlers: LabelHandler[] = []; 309 + 310 + /** Register a module-level handler for label assignment records. */ 311 + export function registerLabelHandler(handler: LabelHandler): void { 312 + labelHandlers.push(handler); 313 + } 314 + 315 + export function indexLabelAssignment( 316 + did: string, 317 + rkey: string, 318 + record: Record<string, unknown>, 319 + ): void { 320 + const subjectUri = record.subject as string; 321 + const labels = record.labels as string[]; 322 + if (!subjectUri || !Array.isArray(labels)) return; 323 + 324 + for (const handler of labelHandlers) { 325 + if (handler(subjectUri, labels, did, rkey)) return; 326 + } 327 + }
+35 -3
packages/feature-requests/src/api/requests.ts
··· 24 24 } from "@exosphere/core/sphere"; 25 25 26 26 const COLLECTION = "site.exosphere.featureRequest.entry"; 27 + const LABEL_COLLECTION = "site.exosphere.label"; 27 28 const MODERATION_COLLECTION = "site.exosphere.moderation"; 28 29 29 30 const app = new Hono<AuthEnv & SphereEnv>(); ··· 176 177 177 178 let pdsUri: string | null = null; 178 179 179 - // Write to PDS for public spheres 180 + // Write to PDS for public spheres (category deprecated — no longer written) 180 181 if (sphereVisibility === "public") { 181 182 const session = c.var.session; 182 183 pdsUri = await putPdsRecord(session, COLLECTION, id, { 183 184 title, 184 185 description, 185 - category, 186 186 subject: sphereOwnerDid, 187 187 }); 188 188 } ··· 204 204 const labels = labelIds?.length 205 205 ? (getLabelsForEntities([id], "feature-request").get(id) ?? []) 206 206 : []; 207 + 208 + // Write label PDS record if labels were set and entity has a PDS URI 209 + if (pdsUri && labels.length > 0) { 210 + const session = c.var.session; 211 + const labelTid = generateRkey(); 212 + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { 213 + subject: pdsUri, 214 + labels: labels.map((l) => l.name), 215 + }); 216 + if (labelPdsUri) { 217 + const db = getDb(); 218 + db.update(featureRequests).set({ labelTid }).where(eq(featureRequests.id, id)).run(); 219 + } 220 + } 207 221 208 222 return c.json({ featureRequest: row ? { ...row, createdAt: tidToDate(id), labels } : row }, 201); 209 223 }); ··· 403 417 const sphereId = c.var.sphereId; 404 418 405 419 const existing = db 406 - .select({ id: featureRequests.id, authorDid: featureRequests.authorDid }) 420 + .select({ 421 + id: featureRequests.id, 422 + authorDid: featureRequests.authorDid, 423 + pdsUri: featureRequests.pdsUri, 424 + }) 407 425 .from(featureRequests) 408 426 .where(and(eq(featureRequests.id, id), eq(featureRequests.sphereId, sphereId))) 409 427 .get(); ··· 426 444 427 445 setEntityLabels(sphereId, id, "feature-request", parsed.data.labelIds); 428 446 const labels = getLabelsForEntities([id], "feature-request").get(id) ?? []; 447 + 448 + // Write label PDS record 449 + if (c.var.sphereVisibility === "public" && existing.pdsUri) { 450 + const session = c.var.session; 451 + const labelTid = generateRkey(); 452 + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { 453 + subject: existing.pdsUri, 454 + labels: labels.map((l) => l.name), 455 + }); 456 + if (labelPdsUri) { 457 + db.update(featureRequests).set({ labelTid }).where(eq(featureRequests.id, id)).run(); 458 + } 459 + } 460 + 429 461 return c.json({ labels }); 430 462 }); 431 463
+1
packages/feature-requests/src/db/schema.ts
··· 32 32 pdsUri: text("pds_uri"), 33 33 hiddenAt: text("hidden_at"), 34 34 moderatedBy: text("moderated_by"), 35 + labelTid: text("label_tid"), 35 36 updatedAt: text("updated_at") 36 37 .notNull() 37 38 .default(sql`(datetime('now'))`),
+43 -4
packages/feature-requests/src/indexer.ts
··· 1 1 import type { ModuleIndexer, JetstreamCommitEvent } from "@exosphere/core/types"; 2 2 import { buildAtUri, parseAtUri } from "@exosphere/core/indexer"; 3 - import { registerModerationHandler, getActiveMemberRole } from "@exosphere/core/sphere"; 3 + import { 4 + registerModerationHandler, 5 + registerLabelHandler, 6 + getActiveMemberRole, 7 + setEntityLabels, 8 + resolveLabelIdsByName, 9 + } from "@exosphere/core/sphere"; 4 10 import { checkPermission } from "@exosphere/core/permissions"; 5 11 import { getDb } from "@exosphere/core/db"; 6 12 import { eq, and } from "@exosphere/core/db/drizzle"; ··· 21 27 handleFeatureRequestModeration, 22 28 } from "./db/operations.ts"; 23 29 24 - // Register this module's moderation handler with core 25 - registerModerationHandler(handleFeatureRequestModeration); 26 - 27 30 const MODULE_NAME = "feature-requests"; 28 31 const COLLECTION = "site.exosphere.featureRequest.entry"; 29 32 const VOTE_COLLECTION = "site.exosphere.featureRequest.vote"; 30 33 const COMMENT_COLLECTION = "site.exosphere.featureRequest.comment"; 31 34 const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequest.commentVote"; 32 35 const STATUS_COLLECTION = "site.exosphere.featureRequest.status"; 36 + 37 + // Register this module's moderation handler with core 38 + registerModerationHandler(handleFeatureRequestModeration); 39 + 40 + // Register label handler: resolve label PDS records targeting feature requests 41 + registerLabelHandler((subjectUri, labelNames, actorDid, tid) => { 42 + const parsed = parseAtUri(subjectUri); 43 + if (!parsed || parsed.collection !== COLLECTION) return false; 44 + 45 + const db = getDb(); 46 + const fr = db 47 + .select({ 48 + id: featureRequests.id, 49 + sphereId: featureRequests.sphereId, 50 + authorDid: featureRequests.authorDid, 51 + labelTid: featureRequests.labelTid, 52 + }) 53 + .from(featureRequests) 54 + .where(eq(featureRequests.id, parsed.rkey)) 55 + .get(); 56 + if (!fr) return false; 57 + 58 + // TID-based ordering: skip if we already have a newer label record 59 + if (fr.labelTid && fr.labelTid >= tid) return true; 60 + 61 + // Permission check: author can always set labels, otherwise need changeStatus 62 + if (fr.authorDid !== actorDid) { 63 + const role = getActiveMemberRole(fr.sphereId, actorDid); 64 + if (!checkPermission(fr.sphereId, MODULE_NAME, "changeStatus", role)) return true; 65 + } 66 + 67 + const labelIds = resolveLabelIdsByName(fr.sphereId, labelNames); 68 + setEntityLabels(fr.sphereId, fr.id, "feature-request", labelIds); 69 + db.update(featureRequests).set({ labelTid: tid }).where(eq(featureRequests.id, fr.id)).run(); 70 + return true; 71 + }); 33 72 34 73 function findSphereForAccess( 35 74 sphereOwnerDid: string,
+34 -1
packages/kanban/src/api/tasks.ts
··· 35 35 } from "@exosphere/core/sphere"; 36 36 37 37 const COLLECTION = "site.exosphere.kanban.entry"; 38 + const LABEL_COLLECTION = "site.exosphere.label"; 38 39 const STATUS_COLLECTION = "site.exosphere.kanban.status"; 39 40 const MODERATION_COLLECTION = "site.exosphere.moderation"; 40 41 ··· 215 216 } 216 217 217 218 const labels = labelIds?.length ? (getLabelsForEntities([id], "kanban-task").get(id) ?? []) : []; 219 + 220 + // Write label PDS record if labels were set and entity has a PDS URI 221 + if (pdsUri && labels.length > 0) { 222 + const session = c.var.session; 223 + const labelTid = generateRkey(); 224 + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { 225 + subject: pdsUri, 226 + labels: labels.map((l) => l.name), 227 + }); 228 + if (labelPdsUri) { 229 + const db = getDb(); 230 + db.update(kanbanTasks).set({ labelTid }).where(eq(kanbanTasks.id, id)).run(); 231 + } 232 + } 218 233 219 234 return c.json({ task: row ? { ...row, createdAt: tidToDate(id), labels } : row }, 201); 220 235 }); ··· 573 588 const sphereId = c.var.sphereId; 574 589 575 590 const existing = db 576 - .select({ id: kanbanTasks.id, authorDid: kanbanTasks.authorDid }) 591 + .select({ 592 + id: kanbanTasks.id, 593 + authorDid: kanbanTasks.authorDid, 594 + pdsUri: kanbanTasks.pdsUri, 595 + }) 577 596 .from(kanbanTasks) 578 597 .where(and(eq(kanbanTasks.id, id), eq(kanbanTasks.sphereId, sphereId))) 579 598 .get(); ··· 596 615 597 616 setEntityLabels(sphereId, id, "kanban-task", parsed.data.labelIds); 598 617 const labels = getLabelsForEntities([id], "kanban-task").get(id) ?? []; 618 + 619 + // Write label PDS record 620 + if (c.var.sphereVisibility === "public" && existing.pdsUri) { 621 + const session = c.var.session; 622 + const labelTid = generateRkey(); 623 + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { 624 + subject: existing.pdsUri, 625 + labels: labels.map((l) => l.name), 626 + }); 627 + if (labelPdsUri) { 628 + db.update(kanbanTasks).set({ labelTid }).where(eq(kanbanTasks.id, id)).run(); 629 + } 630 + } 631 + 599 632 return c.json({ labels }); 600 633 }); 601 634
+1
packages/kanban/src/db/schema.ts
··· 40 40 pdsUri: text("pds_uri"), 41 41 hiddenAt: text("hidden_at"), 42 42 moderatedBy: text("moderated_by"), 43 + labelTid: text("label_tid"), 43 44 updatedAt: text("updated_at") 44 45 .notNull() 45 46 .default(sql`(datetime('now'))`),
+43 -4
packages/kanban/src/indexer.ts
··· 1 1 import type { ModuleIndexer, JetstreamCommitEvent } from "@exosphere/core/types"; 2 2 import { buildAtUri, parseAtUri } from "@exosphere/core/indexer"; 3 - import { registerModerationHandler, getActiveMemberRole } from "@exosphere/core/sphere"; 3 + import { 4 + registerModerationHandler, 5 + registerLabelHandler, 6 + getActiveMemberRole, 7 + setEntityLabels, 8 + resolveLabelIdsByName, 9 + } from "@exosphere/core/sphere"; 4 10 import { checkPermission } from "@exosphere/core/permissions"; 5 11 import { getDb } from "@exosphere/core/db"; 6 12 import { eq, and } from "@exosphere/core/db/drizzle"; ··· 17 23 handleKanbanModeration, 18 24 } from "./db/operations.ts"; 19 25 20 - // Register this module's moderation handler with core 21 - registerModerationHandler(handleKanbanModeration); 22 - 23 26 const MODULE_NAME = "kanban"; 24 27 const COLLECTION = "site.exosphere.kanban.entry"; 25 28 const COMMENT_COLLECTION = "site.exosphere.kanban.comment"; 26 29 const STATUS_COLLECTION = "site.exosphere.kanban.status"; 30 + 31 + // Register this module's moderation handler with core 32 + registerModerationHandler(handleKanbanModeration); 33 + 34 + // Register label handler: resolve label PDS records targeting kanban tasks 35 + registerLabelHandler((subjectUri, labelNames, actorDid, tid) => { 36 + const parsed = parseAtUri(subjectUri); 37 + if (!parsed || parsed.collection !== COLLECTION) return false; 38 + 39 + const db = getDb(); 40 + const task = db 41 + .select({ 42 + id: kanbanTasks.id, 43 + sphereId: kanbanTasks.sphereId, 44 + authorDid: kanbanTasks.authorDid, 45 + labelTid: kanbanTasks.labelTid, 46 + }) 47 + .from(kanbanTasks) 48 + .where(eq(kanbanTasks.id, parsed.rkey)) 49 + .get(); 50 + if (!task) return false; 51 + 52 + // TID-based ordering: skip if we already have a newer label record 53 + if (task.labelTid && task.labelTid >= tid) return true; 54 + 55 + // Permission check: author can always set labels, otherwise need manageTasks 56 + if (task.authorDid !== actorDid) { 57 + const role = getActiveMemberRole(task.sphereId, actorDid); 58 + if (!checkPermission(task.sphereId, MODULE_NAME, "manageTasks", role)) return true; 59 + } 60 + 61 + const labelIds = resolveLabelIdsByName(task.sphereId, labelNames); 62 + setEntityLabels(task.sphereId, task.id, "kanban-task", labelIds); 63 + db.update(kanbanTasks).set({ labelTid: tid }).where(eq(kanbanTasks.id, task.id)).run(); 64 + return true; 65 + }); 27 66 28 67 function findSphereForAccess( 29 68 sphereOwnerDid: string,
+1 -6
packages/kanban/src/ui/components/column.tsx
··· 40 40 if (isOver) headerClasses.push(kbUi.columnHeaderDragOver); 41 41 42 42 return ( 43 - <div 44 - class={kbUi.column} 45 - onDragOver={onDragOver} 46 - onDragLeave={onDragLeave} 47 - onDrop={onDrop} 48 - > 43 + <div class={kbUi.column} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}> 49 44 <div class={headerClasses.join(" ")}> 50 45 <span class={kbUi.columnTitle}>{title}</span> 51 46 <span class={kbUi.columnCount}>{tasks.length}</span>
+16 -4
packages/kanban/src/ui/pages/board.tsx
··· 83 83 </div> 84 84 85 85 <Modal open={showForm} onClose={() => (showForm.value = false)} title="New task"> 86 - <TaskForm columns={columnDefs.value} onCreated={onCreated} initialStatus={formInitialStatus.value} /> 86 + <TaskForm 87 + columns={columnDefs.value} 88 + onCreated={onCreated} 89 + initialStatus={formInitialStatus.value} 90 + /> 87 91 </Modal> 88 92 89 93 {pending && !data ? ( ··· 98 102 {[0, 1].map((j) => ( 99 103 <div key={j} class={kbUi.taskCard} style={{ pointerEvents: "none" }}> 100 104 <div class={ui.skeletonLine} style={{ inlineSize: "40px" }} /> 101 - <div class={ui.skeletonLine} style={{ inlineSize: "90%", marginBlockStart: "4px" }} /> 102 - <div class={ui.skeletonLine} style={{ inlineSize: "50%", marginBlockStart: "4px" }} /> 105 + <div 106 + class={ui.skeletonLine} 107 + style={{ inlineSize: "90%", marginBlockStart: "4px" }} 108 + /> 109 + <div 110 + class={ui.skeletonLine} 111 + style={{ inlineSize: "50%", marginBlockStart: "4px" }} 112 + /> 103 113 </div> 104 114 ))} 105 115 </div> ··· 124 134 onDrop={dnd.onColumnDrop(col.slug)} 125 135 onCardDragStart={dnd.onDragStart} 126 136 onCardDragEnd={dnd.onDragEnd} 127 - onAddTask={isAuthenticated && canCreate.value ? () => openForm(col.slug) : undefined} 137 + onAddTask={ 138 + isAuthenticated && canCreate.value ? () => openForm(col.slug) : undefined 139 + } 128 140 /> 129 141 ))} 130 142 </div>