+2
-1
appview/db/issues.go
+2
-1
appview/db/issues.go
···
11
11
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
13
"tangled.org/core/api/tangled"
14
+
"tangled.org/core/appview/models"
14
15
"tangled.org/core/appview/pagination"
15
16
)
16
17
···
30
31
// optionally, populate this when querying for reverse mappings
31
32
// like comment counts, parent repo etc.
32
33
Comments []IssueComment
33
-
Labels LabelState
34
+
Labels models.LabelState
34
35
Repo *Repo
35
36
}
36
37
+33
-496
appview/db/label.go
+33
-496
appview/db/label.go
···
1
1
package db
2
2
3
3
import (
4
-
"crypto/sha1"
5
4
"database/sql"
6
-
"encoding/hex"
7
-
"errors"
8
5
"fmt"
9
6
"maps"
10
7
"slices"
···
12
9
"time"
13
10
14
11
"github.com/bluesky-social/indigo/atproto/syntax"
15
-
"tangled.sh/tangled.sh/core/api/tangled"
16
-
"tangled.sh/tangled.sh/core/consts"
17
-
)
18
-
19
-
type ConcreteType string
20
-
21
-
const (
22
-
ConcreteTypeNull ConcreteType = "null"
23
-
ConcreteTypeString ConcreteType = "string"
24
-
ConcreteTypeInt ConcreteType = "integer"
25
-
ConcreteTypeBool ConcreteType = "boolean"
26
-
)
27
-
28
-
type ValueTypeFormat string
29
-
30
-
const (
31
-
ValueTypeFormatAny ValueTypeFormat = "any"
32
-
ValueTypeFormatDid ValueTypeFormat = "did"
12
+
"tangled.org/core/appview/models"
33
13
)
34
14
35
-
// ValueType represents an atproto lexicon type definition with constraints
36
-
type ValueType struct {
37
-
Type ConcreteType `json:"type"`
38
-
Format ValueTypeFormat `json:"format,omitempty"`
39
-
Enum []string `json:"enum,omitempty"`
40
-
}
41
-
42
-
func (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
-
50
-
func 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
-
58
-
func (vt ValueType) IsConcreteType() bool {
59
-
return vt.Type == ConcreteTypeNull ||
60
-
vt.Type == ConcreteTypeString ||
61
-
vt.Type == ConcreteTypeInt ||
62
-
vt.Type == ConcreteTypeBool
63
-
}
64
-
65
-
func (vt ValueType) IsNull() bool {
66
-
return vt.Type == ConcreteTypeNull
67
-
}
68
-
69
-
func (vt ValueType) IsString() bool {
70
-
return vt.Type == ConcreteTypeString
71
-
}
72
-
73
-
func (vt ValueType) IsInt() bool {
74
-
return vt.Type == ConcreteTypeInt
75
-
}
76
-
77
-
func (vt ValueType) IsBool() bool {
78
-
return vt.Type == ConcreteTypeBool
79
-
}
80
-
81
-
func (vt ValueType) IsEnum() bool {
82
-
return len(vt.Enum) > 0
83
-
}
84
-
85
-
func (vt ValueType) IsDidFormat() bool {
86
-
return vt.Format == ValueTypeFormatDid
87
-
}
88
-
89
-
func (vt ValueType) IsAnyFormat() bool {
90
-
return vt.Format == ValueTypeFormatAny
91
-
}
92
-
93
-
type 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
-
106
-
func (l *LabelDefinition) AtUri() syntax.ATURI {
107
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
108
-
}
109
-
110
-
func (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
123
-
func 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
-
133
-
func (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
-
143
-
func 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
-
172
-
func 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
15
// no updating type for now
189
-
func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) {
16
+
func AddLabelDefinition(e Execer, l *models.LabelDefinition) (int64, error) {
190
17
result, err := e.Exec(
191
18
`insert into label_definitions (
192
19
did,
···
232
59
return id, nil
233
60
}
234
61
235
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) {
236
-
var labelDefinitions []LabelDefinition
62
+
func DeleteLabelDefinition(e Execer, filters ...filter) error {
63
+
var conditions []string
64
+
var args []any
65
+
for _, filter := range filters {
66
+
conditions = append(conditions, filter.Condition())
67
+
args = append(args, filter.Arg()...)
68
+
}
69
+
whereClause := ""
70
+
if conditions != nil {
71
+
whereClause = " where " + strings.Join(conditions, " and ")
72
+
}
73
+
query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
74
+
_, err := e.Exec(query, args...)
75
+
return err
76
+
}
77
+
78
+
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79
+
var labelDefinitions []models.LabelDefinition
237
80
var conditions []string
238
81
var args []any
239
82
···
275
118
defer rows.Close()
276
119
277
120
for rows.Next() {
278
-
var labelDefinition LabelDefinition
121
+
var labelDefinition models.LabelDefinition
279
122
var createdAt, enumVariants, scopes string
280
123
var color sql.Null[string]
281
124
var multiple int
···
324
167
}
325
168
326
169
// helper to get exactly one label def
327
-
func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) {
170
+
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
328
171
labels, err := GetLabelDefinitions(e, filters...)
329
172
if err != nil {
330
173
return nil, err
···
341
184
return &labels[0], nil
342
185
}
343
186
344
-
type 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
-
356
-
func (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
-
379
-
type LabelOperation string
380
-
381
-
const (
382
-
LabelOperationAdd LabelOperation = "add"
383
-
LabelOperationDel LabelOperation = "del"
384
-
)
385
-
386
-
// a record can create multiple label ops
387
-
func 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
-
423
-
func 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
-
460
-
func AddLabelOp(e Execer, l *LabelOp) (int64, error) {
187
+
func AddLabelOp(e Execer, l *models.LabelOp) (int64, error) {
461
188
now := time.Now()
462
189
result, err := e.Exec(
463
190
`insert into label_ops (
···
500
227
return id, nil
501
228
}
502
229
503
-
func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) {
504
-
var labelOps []LabelOp
230
+
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231
+
var labelOps []models.LabelOp
505
232
var conditions []string
506
233
var args []any
507
234
···
541
268
defer rows.Close()
542
269
543
270
for rows.Next() {
544
-
var labelOp LabelOp
271
+
var labelOp models.LabelOp
545
272
var performedAt, indexedAt string
546
273
547
274
if err := rows.Scan(
···
575
302
}
576
303
577
304
// get labels for a given list of subject URIs
578
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) {
305
+
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
579
306
ops, err := GetLabelOps(e, filters...)
580
307
if err != nil {
581
308
return nil, err
582
309
}
583
310
584
311
// group ops by subject
585
-
opsBySubject := make(map[syntax.ATURI][]LabelOp)
312
+
opsBySubject := make(map[syntax.ATURI][]models.LabelOp)
586
313
for _, op := range ops {
587
314
subject := syntax.ATURI(op.Subject)
588
315
opsBySubject[subject] = append(opsBySubject[subject], op)
···
601
328
}
602
329
603
330
// apply label ops for each subject and collect results
604
-
results := make(map[syntax.ATURI]LabelState)
331
+
results := make(map[syntax.ATURI]models.LabelState)
605
332
for subject, subjectOps := range opsBySubject {
606
-
state := NewLabelState()
333
+
state := models.NewLabelState()
607
334
actx.ApplyLabelOps(state, subjectOps)
608
335
results[subject] = state
609
336
}
···
611
338
return results, nil
612
339
}
613
340
614
-
type set = map[string]struct{}
615
-
616
-
type LabelState struct {
617
-
inner map[string]set
618
-
}
619
-
620
-
func NewLabelState() LabelState {
621
-
return LabelState{
622
-
inner: make(map[string]set),
623
-
}
624
-
}
625
-
626
-
func (s LabelState) Inner() map[string]set {
627
-
return s.inner
628
-
}
629
-
630
-
func (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
642
-
func (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
-
652
-
func (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
-
660
-
type LabelApplicationCtx struct {
661
-
Defs map[string]*LabelDefinition // labelAt -> labelDef
662
-
}
663
-
664
-
var (
665
-
LabelNoOpError = errors.New("no-op")
666
-
)
667
-
668
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) {
341
+
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
669
342
labels, err := GetLabelDefinitions(e, filters...)
670
343
if err != nil {
671
344
return nil, err
672
345
}
673
346
674
-
defs := make(map[string]*LabelDefinition)
347
+
defs := make(map[string]*models.LabelDefinition)
675
348
for _, l := range labels {
676
349
defs[l.AtUri().String()] = &l
677
350
}
678
351
679
-
return &LabelApplicationCtx{defs}, nil
680
-
}
681
-
682
-
func (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
-
737
-
func (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
751
-
func (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.
762
-
func 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
-
801
-
func 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
352
+
return &models.LabelApplicationCtx{defs}, nil
816
353
}
+1
-1
appview/ingester.go
+1
-1
appview/ingester.go
···
923
923
return fmt.Errorf("invalid record: %w", err)
924
924
}
925
925
926
-
def, err := db.LabelDefinitionFromRecord(did, rkey, record)
926
+
def, err := models.LabelDefinitionFromRecord(did, rkey, record)
927
927
if err != nil {
928
928
return fmt.Errorf("failed to parse labeldef from record: %w", err)
929
929
}
+3
-2
appview/issues/issues.go
+3
-2
appview/issues/issues.go
···
19
19
"tangled.org/core/api/tangled"
20
20
"tangled.org/core/appview/config"
21
21
"tangled.org/core/appview/db"
22
+
"tangled.org/core/appview/models"
22
23
"tangled.org/core/appview/notify"
23
24
"tangled.org/core/appview/oauth"
24
25
"tangled.org/core/appview/pages"
···
103
104
return
104
105
}
105
106
106
-
defs := make(map[string]*db.LabelDefinition)
107
+
defs := make(map[string]*models.LabelDefinition)
107
108
for _, l := range labelDefs {
108
109
defs[l.AtUri().String()] = &l
109
110
}
···
796
797
return
797
798
}
798
799
799
-
defs := make(map[string]*db.LabelDefinition)
800
+
defs := make(map[string]*models.LabelDefinition)
800
801
for _, l := range labelDefs {
801
802
defs[l.AtUri().String()] = &l
802
803
}
+10
-9
appview/labels/labels.go
+10
-9
appview/labels/labels.go
···
17
17
"tangled.sh/tangled.sh/core/api/tangled"
18
18
"tangled.sh/tangled.sh/core/appview/db"
19
19
"tangled.sh/tangled.sh/core/appview/middleware"
20
+
"tangled.sh/tangled.sh/core/appview/models"
20
21
"tangled.sh/tangled.sh/core/appview/oauth"
21
22
"tangled.sh/tangled.sh/core/appview/pages"
22
23
"tangled.sh/tangled.sh/core/appview/validator"
···
113
114
return
114
115
}
115
116
116
-
labelState := db.NewLabelState()
117
+
labelState := models.NewLabelState()
117
118
actx.ApplyLabelOps(labelState, existingOps)
118
119
119
-
var labelOps []db.LabelOp
120
+
var labelOps []models.LabelOp
120
121
121
122
// first delete all existing state
122
123
for key, vals := range labelState.Inner() {
123
124
for val := range vals {
124
-
labelOps = append(labelOps, db.LabelOp{
125
+
labelOps = append(labelOps, models.LabelOp{
125
126
Did: did,
126
127
Rkey: rkey,
127
128
Subject: syntax.ATURI(subjectUri),
128
-
Operation: db.LabelOperationDel,
129
+
Operation: models.LabelOperationDel,
129
130
OperandKey: key,
130
131
OperandValue: val,
131
132
PerformedAt: performedAt,
···
141
142
}
142
143
143
144
for _, val := range vals {
144
-
labelOps = append(labelOps, db.LabelOp{
145
+
labelOps = append(labelOps, models.LabelOp{
145
146
Did: did,
146
147
Rkey: rkey,
147
148
Subject: syntax.ATURI(subjectUri),
148
-
Operation: db.LabelOperationAdd,
149
+
Operation: models.LabelOperationAdd,
149
150
OperandKey: key,
150
151
OperandValue: val,
151
152
PerformedAt: performedAt,
···
155
156
}
156
157
157
158
// reduce the opset
158
-
labelOps = db.ReduceLabelOps(labelOps)
159
+
labelOps = models.ReduceLabelOps(labelOps)
159
160
160
161
for i := range labelOps {
161
162
def := actx.Defs[labelOps[i].OperandKey]
···
168
169
// next, apply all ops introduced in this request and filter out ones that are no-ops
169
170
validLabelOps := labelOps[:0]
170
171
for _, op := range labelOps {
171
-
if err = actx.ApplyLabelOp(labelState, op); err != db.LabelNoOpError {
172
+
if err = actx.ApplyLabelOp(labelState, op); err != models.LabelNoOpError {
172
173
validLabelOps = append(validLabelOps, op)
173
174
}
174
175
}
···
180
181
}
181
182
182
183
// create an atproto record of valid ops
183
-
record := db.LabelOpsAsRecord(validLabelOps)
184
+
record := models.LabelOpsAsRecord(validLabelOps)
184
185
185
186
client, err := l.oauth.AuthorizedClient(r)
186
187
if err != nil {
+473
appview/models/label.go
+473
appview/models/label.go
···
1
+
package models
2
+
3
+
import (
4
+
"crypto/sha1"
5
+
"encoding/hex"
6
+
"errors"
7
+
"fmt"
8
+
"slices"
9
+
"time"
10
+
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
"tangled.org/core/api/tangled"
13
+
"tangled.org/core/consts"
14
+
)
15
+
16
+
type ConcreteType string
17
+
18
+
const (
19
+
ConcreteTypeNull ConcreteType = "null"
20
+
ConcreteTypeString ConcreteType = "string"
21
+
ConcreteTypeInt ConcreteType = "integer"
22
+
ConcreteTypeBool ConcreteType = "boolean"
23
+
)
24
+
25
+
type ValueTypeFormat string
26
+
27
+
const (
28
+
ValueTypeFormatAny ValueTypeFormat = "any"
29
+
ValueTypeFormatDid ValueTypeFormat = "did"
30
+
)
31
+
32
+
// ValueType represents an atproto lexicon type definition with constraints
33
+
type ValueType struct {
34
+
Type ConcreteType `json:"type"`
35
+
Format ValueTypeFormat `json:"format,omitempty"`
36
+
Enum []string `json:"enum,omitempty"`
37
+
}
38
+
39
+
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
40
+
return tangled.LabelDefinition_ValueType{
41
+
Type: string(vt.Type),
42
+
Format: string(vt.Format),
43
+
Enum: vt.Enum,
44
+
}
45
+
}
46
+
47
+
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
48
+
return ValueType{
49
+
Type: ConcreteType(record.Type),
50
+
Format: ValueTypeFormat(record.Format),
51
+
Enum: record.Enum,
52
+
}
53
+
}
54
+
55
+
func (vt ValueType) IsConcreteType() bool {
56
+
return vt.Type == ConcreteTypeNull ||
57
+
vt.Type == ConcreteTypeString ||
58
+
vt.Type == ConcreteTypeInt ||
59
+
vt.Type == ConcreteTypeBool
60
+
}
61
+
62
+
func (vt ValueType) IsNull() bool {
63
+
return vt.Type == ConcreteTypeNull
64
+
}
65
+
66
+
func (vt ValueType) IsString() bool {
67
+
return vt.Type == ConcreteTypeString
68
+
}
69
+
70
+
func (vt ValueType) IsInt() bool {
71
+
return vt.Type == ConcreteTypeInt
72
+
}
73
+
74
+
func (vt ValueType) IsBool() bool {
75
+
return vt.Type == ConcreteTypeBool
76
+
}
77
+
78
+
func (vt ValueType) IsEnum() bool {
79
+
return len(vt.Enum) > 0
80
+
}
81
+
82
+
func (vt ValueType) IsDidFormat() bool {
83
+
return vt.Format == ValueTypeFormatDid
84
+
}
85
+
86
+
func (vt ValueType) IsAnyFormat() bool {
87
+
return vt.Format == ValueTypeFormatAny
88
+
}
89
+
90
+
type LabelDefinition struct {
91
+
Id int64
92
+
Did string
93
+
Rkey string
94
+
95
+
Name string
96
+
ValueType ValueType
97
+
Scope []string
98
+
Color *string
99
+
Multiple bool
100
+
Created time.Time
101
+
}
102
+
103
+
func (l *LabelDefinition) AtUri() syntax.ATURI {
104
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
105
+
}
106
+
107
+
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
108
+
vt := l.ValueType.AsRecord()
109
+
return tangled.LabelDefinition{
110
+
Name: l.Name,
111
+
Color: l.Color,
112
+
CreatedAt: l.Created.Format(time.RFC3339),
113
+
Multiple: &l.Multiple,
114
+
Scope: l.Scope,
115
+
ValueType: &vt,
116
+
}
117
+
}
118
+
119
+
// random color for a given seed
120
+
func randomColor(seed string) string {
121
+
hash := sha1.Sum([]byte(seed))
122
+
hexStr := hex.EncodeToString(hash[:])
123
+
r := hexStr[0:2]
124
+
g := hexStr[2:4]
125
+
b := hexStr[4:6]
126
+
127
+
return fmt.Sprintf("#%s%s%s", r, g, b)
128
+
}
129
+
130
+
func (ld LabelDefinition) GetColor() string {
131
+
if ld.Color == nil {
132
+
seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
133
+
color := randomColor(seed)
134
+
return color
135
+
}
136
+
137
+
return *ld.Color
138
+
}
139
+
140
+
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
141
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
142
+
if err != nil {
143
+
created = time.Now()
144
+
}
145
+
146
+
multiple := false
147
+
if record.Multiple != nil {
148
+
multiple = *record.Multiple
149
+
}
150
+
151
+
var vt ValueType
152
+
if record.ValueType != nil {
153
+
vt = ValueTypeFromRecord(*record.ValueType)
154
+
}
155
+
156
+
return &LabelDefinition{
157
+
Did: did,
158
+
Rkey: rkey,
159
+
160
+
Name: record.Name,
161
+
ValueType: vt,
162
+
Scope: record.Scope,
163
+
Color: record.Color,
164
+
Multiple: multiple,
165
+
Created: created,
166
+
}, nil
167
+
}
168
+
169
+
type LabelOp struct {
170
+
Id int64
171
+
Did string
172
+
Rkey string
173
+
Subject syntax.ATURI
174
+
Operation LabelOperation
175
+
OperandKey string
176
+
OperandValue string
177
+
PerformedAt time.Time
178
+
IndexedAt time.Time
179
+
}
180
+
181
+
func (l LabelOp) SortAt() time.Time {
182
+
createdAt := l.PerformedAt
183
+
indexedAt := l.IndexedAt
184
+
185
+
// if we don't have an indexedat, fall back to now
186
+
if indexedAt.IsZero() {
187
+
indexedAt = time.Now()
188
+
}
189
+
190
+
// if createdat is invalid (before epoch), treat as null -> return zero time
191
+
if createdAt.Before(time.UnixMicro(0)) {
192
+
return time.Time{}
193
+
}
194
+
195
+
// if createdat is <= indexedat, use createdat
196
+
if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
197
+
return createdAt
198
+
}
199
+
200
+
// otherwise, createdat is in the future relative to indexedat -> use indexedat
201
+
return indexedAt
202
+
}
203
+
204
+
type LabelOperation string
205
+
206
+
const (
207
+
LabelOperationAdd LabelOperation = "add"
208
+
LabelOperationDel LabelOperation = "del"
209
+
)
210
+
211
+
// a record can create multiple label ops
212
+
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
213
+
performed, err := time.Parse(time.RFC3339, record.PerformedAt)
214
+
if err != nil {
215
+
performed = time.Now()
216
+
}
217
+
218
+
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
219
+
return LabelOp{
220
+
Did: did,
221
+
Rkey: rkey,
222
+
Subject: syntax.ATURI(record.Subject),
223
+
OperandKey: operand.Key,
224
+
OperandValue: operand.Value,
225
+
PerformedAt: performed,
226
+
}
227
+
}
228
+
229
+
var ops []LabelOp
230
+
for _, o := range record.Add {
231
+
if o != nil {
232
+
op := mkOp(o)
233
+
op.Operation = LabelOperationAdd
234
+
ops = append(ops, op)
235
+
}
236
+
}
237
+
for _, o := range record.Delete {
238
+
if o != nil {
239
+
op := mkOp(o)
240
+
op.Operation = LabelOperationDel
241
+
ops = append(ops, op)
242
+
}
243
+
}
244
+
245
+
return ops
246
+
}
247
+
248
+
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
249
+
if len(ops) == 0 {
250
+
return tangled.LabelOp{}
251
+
}
252
+
253
+
// use the first operation to establish common fields
254
+
first := ops[0]
255
+
record := tangled.LabelOp{
256
+
Subject: string(first.Subject),
257
+
PerformedAt: first.PerformedAt.Format(time.RFC3339),
258
+
}
259
+
260
+
var addOperands []*tangled.LabelOp_Operand
261
+
var deleteOperands []*tangled.LabelOp_Operand
262
+
263
+
for _, op := range ops {
264
+
operand := &tangled.LabelOp_Operand{
265
+
Key: op.OperandKey,
266
+
Value: op.OperandValue,
267
+
}
268
+
269
+
switch op.Operation {
270
+
case LabelOperationAdd:
271
+
addOperands = append(addOperands, operand)
272
+
case LabelOperationDel:
273
+
deleteOperands = append(deleteOperands, operand)
274
+
default:
275
+
return tangled.LabelOp{}
276
+
}
277
+
}
278
+
279
+
record.Add = addOperands
280
+
record.Delete = deleteOperands
281
+
282
+
return record
283
+
}
284
+
285
+
type set = map[string]struct{}
286
+
287
+
type LabelState struct {
288
+
inner map[string]set
289
+
}
290
+
291
+
func NewLabelState() LabelState {
292
+
return LabelState{
293
+
inner: make(map[string]set),
294
+
}
295
+
}
296
+
297
+
func (s LabelState) Inner() map[string]set {
298
+
return s.inner
299
+
}
300
+
301
+
func (s LabelState) ContainsLabel(l string) bool {
302
+
if valset, exists := s.inner[l]; exists {
303
+
if valset != nil {
304
+
return true
305
+
}
306
+
}
307
+
308
+
return false
309
+
}
310
+
311
+
// go maps behavior in templates make this necessary,
312
+
// indexing a map and getting `set` in return is apparently truthy
313
+
func (s LabelState) ContainsLabelAndVal(l, v string) bool {
314
+
if valset, exists := s.inner[l]; exists {
315
+
if _, exists := valset[v]; exists {
316
+
return true
317
+
}
318
+
}
319
+
320
+
return false
321
+
}
322
+
323
+
func (s LabelState) GetValSet(l string) set {
324
+
if valset, exists := s.inner[l]; exists {
325
+
return valset
326
+
} else {
327
+
return make(set)
328
+
}
329
+
}
330
+
331
+
type LabelApplicationCtx struct {
332
+
Defs map[string]*LabelDefinition // labelAt -> labelDef
333
+
}
334
+
335
+
var (
336
+
LabelNoOpError = errors.New("no-op")
337
+
)
338
+
339
+
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
340
+
def, ok := c.Defs[op.OperandKey]
341
+
if !ok {
342
+
// this def was deleted, but an op exists, so we just skip over the op
343
+
return nil
344
+
}
345
+
346
+
switch op.Operation {
347
+
case LabelOperationAdd:
348
+
// if valueset is empty, init it
349
+
if state.inner[op.OperandKey] == nil {
350
+
state.inner[op.OperandKey] = make(set)
351
+
}
352
+
353
+
// if valueset is populated & this val alr exists, this labelop is a noop
354
+
if valueSet, exists := state.inner[op.OperandKey]; exists {
355
+
if _, exists = valueSet[op.OperandValue]; exists {
356
+
return LabelNoOpError
357
+
}
358
+
}
359
+
360
+
if def.Multiple {
361
+
// append to set
362
+
state.inner[op.OperandKey][op.OperandValue] = struct{}{}
363
+
} else {
364
+
// reset to just this value
365
+
state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
366
+
}
367
+
368
+
case LabelOperationDel:
369
+
// if label DNE, then deletion is a no-op
370
+
if valueSet, exists := state.inner[op.OperandKey]; !exists {
371
+
return LabelNoOpError
372
+
} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
373
+
return LabelNoOpError
374
+
}
375
+
376
+
if def.Multiple {
377
+
// remove from set
378
+
delete(state.inner[op.OperandKey], op.OperandValue)
379
+
} else {
380
+
// reset the entire label
381
+
delete(state.inner, op.OperandKey)
382
+
}
383
+
384
+
// if the map becomes empty, then set it to nil, this is just the inverse of add
385
+
if len(state.inner[op.OperandKey]) == 0 {
386
+
state.inner[op.OperandKey] = nil
387
+
}
388
+
389
+
}
390
+
391
+
return nil
392
+
}
393
+
394
+
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
395
+
// sort label ops in sort order first
396
+
slices.SortFunc(ops, func(a, b LabelOp) int {
397
+
return a.SortAt().Compare(b.SortAt())
398
+
})
399
+
400
+
// apply ops in sequence
401
+
for _, o := range ops {
402
+
_ = c.ApplyLabelOp(state, o)
403
+
}
404
+
}
405
+
406
+
// IsInverse checks if one label operation is the inverse of another
407
+
// returns true if one is an add and the other is a delete with the same key and value
408
+
func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
409
+
if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
410
+
return false
411
+
}
412
+
413
+
return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
414
+
(op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
415
+
}
416
+
417
+
// removes pairs of label operations that are inverses of each other
418
+
// from the given slice. the function preserves the order of remaining operations.
419
+
func ReduceLabelOps(ops []LabelOp) []LabelOp {
420
+
if len(ops) <= 1 {
421
+
return ops
422
+
}
423
+
424
+
keep := make([]bool, len(ops))
425
+
for i := range keep {
426
+
keep[i] = true
427
+
}
428
+
429
+
for i := range ops {
430
+
if !keep[i] {
431
+
continue
432
+
}
433
+
434
+
for j := i + 1; j < len(ops); j++ {
435
+
if !keep[j] {
436
+
continue
437
+
}
438
+
439
+
if ops[i].IsInverse(ops[j]) {
440
+
keep[i] = false
441
+
keep[j] = false
442
+
break // move to next i since this one is now eliminated
443
+
}
444
+
}
445
+
}
446
+
447
+
// build result slice with only kept operations
448
+
var result []LabelOp
449
+
for i, op := range ops {
450
+
if keep[i] {
451
+
result = append(result, op)
452
+
}
453
+
}
454
+
455
+
return result
456
+
}
457
+
458
+
func DefaultLabelDefs() []string {
459
+
rkeys := []string{
460
+
"wontfix",
461
+
"duplicate",
462
+
"assignee",
463
+
"good-first-issue",
464
+
"documentation",
465
+
}
466
+
467
+
defs := make([]string, len(rkeys))
468
+
for i, r := range rkeys {
469
+
defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
470
+
}
471
+
472
+
return defs
473
+
}
+8
-8
appview/pages/pages.go
+8
-8
appview/pages/pages.go
···
841
841
type RepoGeneralSettingsParams struct {
842
842
LoggedInUser *oauth.User
843
843
RepoInfo repoinfo.RepoInfo
844
-
Labels []db.LabelDefinition
845
-
DefaultLabels []db.LabelDefinition
844
+
Labels []models.LabelDefinition
845
+
DefaultLabels []models.LabelDefinition
846
846
SubscribedLabels map[string]struct{}
847
847
Active string
848
848
Tabs []map[string]any
···
890
890
RepoInfo repoinfo.RepoInfo
891
891
Active string
892
892
Issues []db.Issue
893
-
LabelDefs map[string]*db.LabelDefinition
893
+
LabelDefs map[string]*models.LabelDefinition
894
894
Page pagination.Page
895
895
FilteringByOpen bool
896
896
}
···
906
906
Active string
907
907
Issue *db.Issue
908
908
CommentList []db.CommentListItem
909
-
LabelDefs map[string]*db.LabelDefinition
909
+
LabelDefs map[string]*models.LabelDefinition
910
910
911
911
OrderedReactionKinds []db.ReactionKind
912
912
Reactions map[db.ReactionKind]int
···
1236
1236
type LabelPanelParams struct {
1237
1237
LoggedInUser *oauth.User
1238
1238
RepoInfo repoinfo.RepoInfo
1239
-
Defs map[string]*db.LabelDefinition
1239
+
Defs map[string]*models.LabelDefinition
1240
1240
Subject string
1241
-
State db.LabelState
1241
+
State models.LabelState
1242
1242
}
1243
1243
1244
1244
func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
···
1248
1248
type EditLabelPanelParams struct {
1249
1249
LoggedInUser *oauth.User
1250
1250
RepoInfo repoinfo.RepoInfo
1251
-
Defs map[string]*db.LabelDefinition
1251
+
Defs map[string]*models.LabelDefinition
1252
1252
Subject string
1253
-
State db.LabelState
1253
+
State models.LabelState
1254
1254
}
1255
1255
1256
1256
func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
+8
-8
appview/repo/repo.go
+8
-8
appview/repo/repo.go
···
1005
1005
concreteType = "null"
1006
1006
}
1007
1007
1008
-
format := db.ValueTypeFormatAny
1008
+
format := models.ValueTypeFormatAny
1009
1009
if valueFormat == "did" {
1010
-
format = db.ValueTypeFormatDid
1010
+
format = models.ValueTypeFormatDid
1011
1011
}
1012
1012
1013
-
valueType := db.ValueType{
1014
-
Type: db.ConcreteType(concreteType),
1013
+
valueType := models.ValueType{
1014
+
Type: models.ConcreteType(concreteType),
1015
1015
Format: format,
1016
1016
Enum: variants,
1017
1017
}
1018
1018
1019
-
label := db.LabelDefinition{
1019
+
label := models.LabelDefinition{
1020
1020
Did: user.Did,
1021
1021
Rkey: tid.TID(),
1022
1022
Name: name,
···
1396
1396
return
1397
1397
}
1398
1398
1399
-
defs := make(map[string]*db.LabelDefinition)
1399
+
defs := make(map[string]*models.LabelDefinition)
1400
1400
for _, l := range labelDefs {
1401
1401
defs[l.AtUri().String()] = &l
1402
1402
}
···
1444
1444
return
1445
1445
}
1446
1446
1447
-
defs := make(map[string]*db.LabelDefinition)
1447
+
defs := make(map[string]*models.LabelDefinition)
1448
1448
for _, l := range labelDefs {
1449
1449
defs[l.AtUri().String()] = &l
1450
1450
}
···
1895
1895
return
1896
1896
}
1897
1897
1898
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))
1898
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs()))
1899
1899
if err != nil {
1900
1900
log.Println("failed to fetch labels", err)
1901
1901
rp.pages.Error503(w)
+12
-12
appview/validator/label.go
+12
-12
appview/validator/label.go
···
9
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
10
"golang.org/x/exp/slices"
11
11
"tangled.sh/tangled.sh/core/api/tangled"
12
-
"tangled.sh/tangled.sh/core/appview/db"
12
+
"tangled.sh/tangled.sh/core/appview/models"
13
13
)
14
14
15
15
var (
···
21
21
validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
22
22
)
23
23
24
-
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
24
+
func (v *Validator) ValidateLabelDefinition(label *models.LabelDefinition) error {
25
25
if label.Name == "" {
26
26
return fmt.Errorf("label name is empty")
27
27
}
···
95
95
return nil
96
96
}
97
97
98
-
func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
98
+
func (v *Validator) ValidateLabelOp(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error {
99
99
if labelDef == nil {
100
100
return fmt.Errorf("label definition is required")
101
101
}
···
108
108
return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey)
109
109
}
110
110
111
-
if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel {
111
+
if labelOp.Operation != models.LabelOperationAdd && labelOp.Operation != models.LabelOperationDel {
112
112
return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation)
113
113
}
114
114
···
131
131
return nil
132
132
}
133
133
134
-
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
134
+
func (v *Validator) validateOperandValue(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error {
135
135
valueType := labelDef.ValueType
136
136
137
137
// this is permitted, it "unsets" a label
138
138
if labelOp.OperandValue == "" {
139
-
labelOp.Operation = db.LabelOperationDel
139
+
labelOp.Operation = models.LabelOperationDel
140
140
return nil
141
141
}
142
142
143
143
switch valueType.Type {
144
-
case db.ConcreteTypeNull:
144
+
case models.ConcreteTypeNull:
145
145
// For null type, value should be empty
146
146
if labelOp.OperandValue != "null" {
147
147
return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue)
148
148
}
149
149
150
-
case db.ConcreteTypeString:
150
+
case models.ConcreteTypeString:
151
151
// For string type, validate enum constraints if present
152
152
if valueType.IsEnum() {
153
153
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
···
156
156
}
157
157
158
158
switch valueType.Format {
159
-
case db.ValueTypeFormatDid:
159
+
case models.ValueTypeFormatDid:
160
160
id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue)
161
161
if err != nil {
162
162
return fmt.Errorf("failed to resolve did/handle: %w", err)
···
164
164
165
165
labelOp.OperandValue = id.DID.String()
166
166
167
-
case db.ValueTypeFormatAny, "":
167
+
case models.ValueTypeFormatAny, "":
168
168
default:
169
169
return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
170
170
}
171
171
172
-
case db.ConcreteTypeInt:
172
+
case models.ConcreteTypeInt:
173
173
if labelOp.OperandValue == "" {
174
174
return fmt.Errorf("integer type requires non-empty value")
175
175
}
···
183
183
}
184
184
}
185
185
186
-
case db.ConcreteTypeBool:
186
+
case models.ConcreteTypeBool:
187
187
if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" {
188
188
return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue)
189
189
}