forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db 2 3import ( 4 "crypto/sha1" 5 "database/sql" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "maps" 10 "slices" 11 "strings" 12 "time" 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "tangled.sh/tangled.sh/core/api/tangled" 16 "tangled.sh/tangled.sh/core/consts" 17) 18 19type ConcreteType string 20 21const ( 22 ConcreteTypeNull ConcreteType = "null" 23 ConcreteTypeString ConcreteType = "string" 24 ConcreteTypeInt ConcreteType = "integer" 25 ConcreteTypeBool ConcreteType = "boolean" 26) 27 28type ValueTypeFormat string 29 30const ( 31 ValueTypeFormatAny ValueTypeFormat = "any" 32 ValueTypeFormatDid ValueTypeFormat = "did" 33) 34 35// ValueType represents an atproto lexicon type definition with constraints 36type ValueType struct { 37 Type ConcreteType `json:"type"` 38 Format ValueTypeFormat `json:"format,omitempty"` 39 Enum []string `json:"enum,omitempty"` 40} 41 42func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType { 43 return tangled.LabelDefinition_ValueType{ 44 Type: string(vt.Type), 45 Format: string(vt.Format), 46 Enum: vt.Enum, 47 } 48} 49 50func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType { 51 return ValueType{ 52 Type: ConcreteType(record.Type), 53 Format: ValueTypeFormat(record.Format), 54 Enum: record.Enum, 55 } 56} 57 58func (vt ValueType) IsConcreteType() bool { 59 return vt.Type == ConcreteTypeNull || 60 vt.Type == ConcreteTypeString || 61 vt.Type == ConcreteTypeInt || 62 vt.Type == ConcreteTypeBool 63} 64 65func (vt ValueType) IsNull() bool { 66 return vt.Type == ConcreteTypeNull 67} 68 69func (vt ValueType) IsString() bool { 70 return vt.Type == ConcreteTypeString 71} 72 73func (vt ValueType) IsInt() bool { 74 return vt.Type == ConcreteTypeInt 75} 76 77func (vt ValueType) IsBool() bool { 78 return vt.Type == ConcreteTypeBool 79} 80 81func (vt ValueType) IsEnum() bool { 82 return len(vt.Enum) > 0 83} 84 85func (vt ValueType) IsDidFormat() bool { 86 return vt.Format == ValueTypeFormatDid 87} 88 89func (vt ValueType) IsAnyFormat() bool { 90 return vt.Format == ValueTypeFormatAny 91} 92 93type LabelDefinition struct { 94 Id int64 95 Did string 96 Rkey string 97 98 Name string 99 ValueType ValueType 100 Scope []string 101 Color *string 102 Multiple bool 103 Created time.Time 104} 105 106func (l *LabelDefinition) AtUri() syntax.ATURI { 107 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 108} 109 110func (l *LabelDefinition) AsRecord() tangled.LabelDefinition { 111 vt := l.ValueType.AsRecord() 112 return tangled.LabelDefinition{ 113 Name: l.Name, 114 Color: l.Color, 115 CreatedAt: l.Created.Format(time.RFC3339), 116 Multiple: &l.Multiple, 117 Scope: l.Scope, 118 ValueType: &vt, 119 } 120} 121 122// random color for a given seed 123func randomColor(seed string) string { 124 hash := sha1.Sum([]byte(seed)) 125 hexStr := hex.EncodeToString(hash[:]) 126 r := hexStr[0:2] 127 g := hexStr[2:4] 128 b := hexStr[4:6] 129 130 return fmt.Sprintf("#%s%s%s", r, g, b) 131} 132 133func (ld LabelDefinition) GetColor() string { 134 if ld.Color == nil { 135 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey) 136 color := randomColor(seed) 137 return color 138 } 139 140 return *ld.Color 141} 142 143func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) { 144 created, err := time.Parse(time.RFC3339, record.CreatedAt) 145 if err != nil { 146 created = time.Now() 147 } 148 149 multiple := false 150 if record.Multiple != nil { 151 multiple = *record.Multiple 152 } 153 154 var vt ValueType 155 if record.ValueType != nil { 156 vt = ValueTypeFromRecord(*record.ValueType) 157 } 158 159 return &LabelDefinition{ 160 Did: did, 161 Rkey: rkey, 162 163 Name: record.Name, 164 ValueType: vt, 165 Scope: record.Scope, 166 Color: record.Color, 167 Multiple: multiple, 168 Created: created, 169 }, nil 170} 171 172func DeleteLabelDefinition(e Execer, filters ...filter) error { 173 var conditions []string 174 var args []any 175 for _, filter := range filters { 176 conditions = append(conditions, filter.Condition()) 177 args = append(args, filter.Arg()...) 178 } 179 whereClause := "" 180 if conditions != nil { 181 whereClause = " where " + strings.Join(conditions, " and ") 182 } 183 query := fmt.Sprintf(`delete from label_definitions %s`, whereClause) 184 _, err := e.Exec(query, args...) 185 return err 186} 187 188// no updating type for now 189func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) { 190 result, err := e.Exec( 191 `insert into label_definitions ( 192 did, 193 rkey, 194 name, 195 value_type, 196 value_format, 197 value_enum, 198 scope, 199 color, 200 multiple, 201 created 202 ) 203 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 204 on conflict(did, rkey) do update set 205 name = excluded.name, 206 scope = excluded.scope, 207 color = excluded.color, 208 multiple = excluded.multiple`, 209 l.Did, 210 l.Rkey, 211 l.Name, 212 l.ValueType.Type, 213 l.ValueType.Format, 214 strings.Join(l.ValueType.Enum, ","), 215 strings.Join(l.Scope, ","), 216 l.Color, 217 l.Multiple, 218 l.Created.Format(time.RFC3339), 219 time.Now().Format(time.RFC3339), 220 ) 221 if err != nil { 222 return 0, err 223 } 224 225 id, err := result.LastInsertId() 226 if err != nil { 227 return 0, err 228 } 229 230 l.Id = id 231 232 return id, nil 233} 234 235func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) { 236 var labelDefinitions []LabelDefinition 237 var conditions []string 238 var args []any 239 240 for _, filter := range filters { 241 conditions = append(conditions, filter.Condition()) 242 args = append(args, filter.Arg()...) 243 } 244 245 whereClause := "" 246 if conditions != nil { 247 whereClause = " where " + strings.Join(conditions, " and ") 248 } 249 250 query := fmt.Sprintf( 251 ` 252 select 253 id, 254 did, 255 rkey, 256 name, 257 value_type, 258 value_format, 259 value_enum, 260 scope, 261 color, 262 multiple, 263 created 264 from label_definitions 265 %s 266 order by created 267 `, 268 whereClause, 269 ) 270 271 rows, err := e.Query(query, args...) 272 if err != nil { 273 return nil, err 274 } 275 defer rows.Close() 276 277 for rows.Next() { 278 var labelDefinition LabelDefinition 279 var createdAt, enumVariants, scopes string 280 var color sql.Null[string] 281 var multiple int 282 283 if err := rows.Scan( 284 &labelDefinition.Id, 285 &labelDefinition.Did, 286 &labelDefinition.Rkey, 287 &labelDefinition.Name, 288 &labelDefinition.ValueType.Type, 289 &labelDefinition.ValueType.Format, 290 &enumVariants, 291 &scopes, 292 &color, 293 &multiple, 294 &createdAt, 295 ); err != nil { 296 return nil, err 297 } 298 299 labelDefinition.Created, err = time.Parse(time.RFC3339, createdAt) 300 if err != nil { 301 labelDefinition.Created = time.Now() 302 } 303 304 if color.Valid { 305 labelDefinition.Color = &color.V 306 } 307 308 if multiple != 0 { 309 labelDefinition.Multiple = true 310 } 311 312 if enumVariants != "" { 313 labelDefinition.ValueType.Enum = strings.Split(enumVariants, ",") 314 } 315 316 for s := range strings.SplitSeq(scopes, ",") { 317 labelDefinition.Scope = append(labelDefinition.Scope, s) 318 } 319 320 labelDefinitions = append(labelDefinitions, labelDefinition) 321 } 322 323 return labelDefinitions, nil 324} 325 326// helper to get exactly one label def 327func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) { 328 labels, err := GetLabelDefinitions(e, filters...) 329 if err != nil { 330 return nil, err 331 } 332 333 if labels == nil { 334 return nil, sql.ErrNoRows 335 } 336 337 if len(labels) != 1 { 338 return nil, fmt.Errorf("too many rows returned") 339 } 340 341 return &labels[0], nil 342} 343 344type LabelOp struct { 345 Id int64 346 Did string 347 Rkey string 348 Subject syntax.ATURI 349 Operation LabelOperation 350 OperandKey string 351 OperandValue string 352 PerformedAt time.Time 353 IndexedAt time.Time 354} 355 356func (l LabelOp) SortAt() time.Time { 357 createdAt := l.PerformedAt 358 indexedAt := l.IndexedAt 359 360 // if we don't have an indexedat, fall back to now 361 if indexedAt.IsZero() { 362 indexedAt = time.Now() 363 } 364 365 // if createdat is invalid (before epoch), treat as null -> return zero time 366 if createdAt.Before(time.UnixMicro(0)) { 367 return time.Time{} 368 } 369 370 // if createdat is <= indexedat, use createdat 371 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) { 372 return createdAt 373 } 374 375 // otherwise, createdat is in the future relative to indexedat -> use indexedat 376 return indexedAt 377} 378 379type LabelOperation string 380 381const ( 382 LabelOperationAdd LabelOperation = "add" 383 LabelOperationDel LabelOperation = "del" 384) 385 386// a record can create multiple label ops 387func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { 388 performed, err := time.Parse(time.RFC3339, record.PerformedAt) 389 if err != nil { 390 performed = time.Now() 391 } 392 393 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { 394 return LabelOp{ 395 Did: did, 396 Rkey: rkey, 397 Subject: syntax.ATURI(record.Subject), 398 OperandKey: operand.Key, 399 OperandValue: operand.Value, 400 PerformedAt: performed, 401 } 402 } 403 404 var ops []LabelOp 405 for _, o := range record.Add { 406 if o != nil { 407 op := mkOp(o) 408 op.Operation = LabelOperationAdd 409 ops = append(ops, op) 410 } 411 } 412 for _, o := range record.Delete { 413 if o != nil { 414 op := mkOp(o) 415 op.Operation = LabelOperationDel 416 ops = append(ops, op) 417 } 418 } 419 420 return ops 421} 422 423func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { 424 if len(ops) == 0 { 425 return tangled.LabelOp{} 426 } 427 428 // use the first operation to establish common fields 429 first := ops[0] 430 record := tangled.LabelOp{ 431 Subject: string(first.Subject), 432 PerformedAt: first.PerformedAt.Format(time.RFC3339), 433 } 434 435 var addOperands []*tangled.LabelOp_Operand 436 var deleteOperands []*tangled.LabelOp_Operand 437 438 for _, op := range ops { 439 operand := &tangled.LabelOp_Operand{ 440 Key: op.OperandKey, 441 Value: op.OperandValue, 442 } 443 444 switch op.Operation { 445 case LabelOperationAdd: 446 addOperands = append(addOperands, operand) 447 case LabelOperationDel: 448 deleteOperands = append(deleteOperands, operand) 449 default: 450 return tangled.LabelOp{} 451 } 452 } 453 454 record.Add = addOperands 455 record.Delete = deleteOperands 456 457 return record 458} 459 460func AddLabelOp(e Execer, l *LabelOp) (int64, error) { 461 now := time.Now() 462 result, err := e.Exec( 463 `insert into label_ops ( 464 did, 465 rkey, 466 subject, 467 operation, 468 operand_key, 469 operand_value, 470 performed, 471 indexed 472 ) 473 values (?, ?, ?, ?, ?, ?, ?, ?) 474 on conflict(did, rkey, subject, operand_key, operand_value) do update set 475 operation = excluded.operation, 476 operand_value = excluded.operand_value, 477 performed = excluded.performed, 478 indexed = excluded.indexed`, 479 l.Did, 480 l.Rkey, 481 l.Subject.String(), 482 string(l.Operation), 483 l.OperandKey, 484 l.OperandValue, 485 l.PerformedAt.Format(time.RFC3339), 486 now.Format(time.RFC3339), 487 ) 488 if err != nil { 489 return 0, err 490 } 491 492 id, err := result.LastInsertId() 493 if err != nil { 494 return 0, err 495 } 496 497 l.Id = id 498 l.IndexedAt = now 499 500 return id, nil 501} 502 503func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) { 504 var labelOps []LabelOp 505 var conditions []string 506 var args []any 507 508 for _, filter := range filters { 509 conditions = append(conditions, filter.Condition()) 510 args = append(args, filter.Arg()...) 511 } 512 513 whereClause := "" 514 if conditions != nil { 515 whereClause = " where " + strings.Join(conditions, " and ") 516 } 517 518 query := fmt.Sprintf( 519 ` 520 select 521 id, 522 did, 523 rkey, 524 subject, 525 operation, 526 operand_key, 527 operand_value, 528 performed, 529 indexed 530 from label_ops 531 %s 532 order by indexed 533 `, 534 whereClause, 535 ) 536 537 rows, err := e.Query(query, args...) 538 if err != nil { 539 return nil, err 540 } 541 defer rows.Close() 542 543 for rows.Next() { 544 var labelOp LabelOp 545 var performedAt, indexedAt string 546 547 if err := rows.Scan( 548 &labelOp.Id, 549 &labelOp.Did, 550 &labelOp.Rkey, 551 &labelOp.Subject, 552 &labelOp.Operation, 553 &labelOp.OperandKey, 554 &labelOp.OperandValue, 555 &performedAt, 556 &indexedAt, 557 ); err != nil { 558 return nil, err 559 } 560 561 labelOp.PerformedAt, err = time.Parse(time.RFC3339, performedAt) 562 if err != nil { 563 labelOp.PerformedAt = time.Now() 564 } 565 566 labelOp.IndexedAt, err = time.Parse(time.RFC3339, indexedAt) 567 if err != nil { 568 labelOp.IndexedAt = time.Now() 569 } 570 571 labelOps = append(labelOps, labelOp) 572 } 573 574 return labelOps, nil 575} 576 577// get labels for a given list of subject URIs 578func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) { 579 ops, err := GetLabelOps(e, filters...) 580 if err != nil { 581 return nil, err 582 } 583 584 // group ops by subject 585 opsBySubject := make(map[syntax.ATURI][]LabelOp) 586 for _, op := range ops { 587 subject := syntax.ATURI(op.Subject) 588 opsBySubject[subject] = append(opsBySubject[subject], op) 589 } 590 591 // get all unique labelats for creating the context 592 labelAtSet := make(map[string]bool) 593 for _, op := range ops { 594 labelAtSet[op.OperandKey] = true 595 } 596 labelAts := slices.Collect(maps.Keys(labelAtSet)) 597 598 actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts)) 599 if err != nil { 600 return nil, err 601 } 602 603 // apply label ops for each subject and collect results 604 results := make(map[syntax.ATURI]LabelState) 605 for subject, subjectOps := range opsBySubject { 606 state := NewLabelState() 607 actx.ApplyLabelOps(state, subjectOps) 608 results[subject] = state 609 } 610 611 return results, nil 612} 613 614type set = map[string]struct{} 615 616type LabelState struct { 617 inner map[string]set 618} 619 620func NewLabelState() LabelState { 621 return LabelState{ 622 inner: make(map[string]set), 623 } 624} 625 626func (s LabelState) Inner() map[string]set { 627 return s.inner 628} 629 630func (s LabelState) ContainsLabel(l string) bool { 631 if valset, exists := s.inner[l]; exists { 632 if valset != nil { 633 return true 634 } 635 } 636 637 return false 638} 639 640// go maps behavior in templates make this necessary, 641// indexing a map and getting `set` in return is apparently truthy 642func (s LabelState) ContainsLabelAndVal(l, v string) bool { 643 if valset, exists := s.inner[l]; exists { 644 if _, exists := valset[v]; exists { 645 return true 646 } 647 } 648 649 return false 650} 651 652func (s LabelState) GetValSet(l string) set { 653 if valset, exists := s.inner[l]; exists { 654 return valset 655 } else { 656 return make(set) 657 } 658} 659 660type LabelApplicationCtx struct { 661 Defs map[string]*LabelDefinition // labelAt -> labelDef 662} 663 664var ( 665 LabelNoOpError = errors.New("no-op") 666) 667 668func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) { 669 labels, err := GetLabelDefinitions(e, filters...) 670 if err != nil { 671 return nil, err 672 } 673 674 defs := make(map[string]*LabelDefinition) 675 for _, l := range labels { 676 defs[l.AtUri().String()] = &l 677 } 678 679 return &LabelApplicationCtx{defs}, nil 680} 681 682func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { 683 def, ok := c.Defs[op.OperandKey] 684 if !ok { 685 // this def was deleted, but an op exists, so we just skip over the op 686 return nil 687 } 688 689 switch op.Operation { 690 case LabelOperationAdd: 691 // if valueset is empty, init it 692 if state.inner[op.OperandKey] == nil { 693 state.inner[op.OperandKey] = make(set) 694 } 695 696 // if valueset is populated & this val alr exists, this labelop is a noop 697 if valueSet, exists := state.inner[op.OperandKey]; exists { 698 if _, exists = valueSet[op.OperandValue]; exists { 699 return LabelNoOpError 700 } 701 } 702 703 if def.Multiple { 704 // append to set 705 state.inner[op.OperandKey][op.OperandValue] = struct{}{} 706 } else { 707 // reset to just this value 708 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} 709 } 710 711 case LabelOperationDel: 712 // if label DNE, then deletion is a no-op 713 if valueSet, exists := state.inner[op.OperandKey]; !exists { 714 return LabelNoOpError 715 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op 716 return LabelNoOpError 717 } 718 719 if def.Multiple { 720 // remove from set 721 delete(state.inner[op.OperandKey], op.OperandValue) 722 } else { 723 // reset the entire label 724 delete(state.inner, op.OperandKey) 725 } 726 727 // if the map becomes empty, then set it to nil, this is just the inverse of add 728 if len(state.inner[op.OperandKey]) == 0 { 729 state.inner[op.OperandKey] = nil 730 } 731 732 } 733 734 return nil 735} 736 737func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 738 // sort label ops in sort order first 739 slices.SortFunc(ops, func(a, b LabelOp) int { 740 return a.SortAt().Compare(b.SortAt()) 741 }) 742 743 // apply ops in sequence 744 for _, o := range ops { 745 _ = c.ApplyLabelOp(state, o) 746 } 747} 748 749// IsInverse checks if one label operation is the inverse of another 750// returns true if one is an add and the other is a delete with the same key and value 751func (op1 LabelOp) IsInverse(op2 LabelOp) bool { 752 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { 753 return false 754 } 755 756 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || 757 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) 758} 759 760// removes pairs of label operations that are inverses of each other 761// from the given slice. the function preserves the order of remaining operations. 762func ReduceLabelOps(ops []LabelOp) []LabelOp { 763 if len(ops) <= 1 { 764 return ops 765 } 766 767 keep := make([]bool, len(ops)) 768 for i := range keep { 769 keep[i] = true 770 } 771 772 for i := range ops { 773 if !keep[i] { 774 continue 775 } 776 777 for j := i + 1; j < len(ops); j++ { 778 if !keep[j] { 779 continue 780 } 781 782 if ops[i].IsInverse(ops[j]) { 783 keep[i] = false 784 keep[j] = false 785 break // move to next i since this one is now eliminated 786 } 787 } 788 } 789 790 // build result slice with only kept operations 791 var result []LabelOp 792 for i, op := range ops { 793 if keep[i] { 794 result = append(result, op) 795 } 796 } 797 798 return result 799} 800 801func DefaultLabelDefs() []string { 802 rkeys := []string{ 803 "wontfix", 804 "duplicate", 805 "assignee", 806 "good-first-issue", 807 "documentation", 808 } 809 810 defs := make([]string, len(rkeys)) 811 for i, r := range rkeys { 812 defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r) 813 } 814 815 return defs 816}