+10
-10
appview/db/label.go
+10
-10
appview/db/label.go
···
6
6
"encoding/hex"
7
7
"errors"
8
8
"fmt"
9
-
"log"
10
9
"maps"
11
10
"slices"
12
11
"strings"
···
80
79
81
80
func (vt ValueType) IsEnumType() bool {
82
81
return len(vt.Enum) > 0
82
+
}
83
+
84
+
func (vt ValueType) IsDidFormat() bool {
85
+
return vt.Format == ValueTypeFormatDid
86
+
}
87
+
88
+
func (vt ValueType) IsAnyFormat() bool {
89
+
return vt.Format == ValueTypeFormatAny
83
90
}
84
91
85
92
type LabelDefinition struct {
···
595
602
results[subject] = state
596
603
}
597
604
598
-
log.Println("results for get labels", "s", results)
599
-
600
605
return results, nil
601
606
}
602
607
···
631
636
}
632
637
633
638
type LabelApplicationCtx struct {
634
-
defs map[string]*LabelDefinition // labelAt -> labelDef
639
+
Defs map[string]*LabelDefinition // labelAt -> labelDef
635
640
}
636
641
637
642
var (
···
653
658
}
654
659
655
660
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
656
-
def := c.defs[op.OperandKey]
661
+
def := c.Defs[op.OperandKey]
657
662
658
663
switch op.Operation {
659
664
case LabelOperationAdd:
···
714
719
_ = c.ApplyLabelOp(state, o)
715
720
}
716
721
}
717
-
718
-
type Label struct {
719
-
def *LabelDefinition
720
-
val set
721
-
}
+5
-1
appview/issues/issues.go
+5
-1
appview/issues/issues.go
···
92
92
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
93
93
}
94
94
95
-
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
95
+
labelDefs, err := db.GetLabelDefinitions(
96
+
rp.db,
97
+
db.FilterIn("at_uri", f.Repo.Labels),
98
+
db.FilterEq("scope", tangled.RepoIssueNSID),
99
+
)
96
100
if err != nil {
97
101
log.Println("failed to fetch labels", err)
98
102
rp.pages.Error503(w)
+14
-2
appview/pages/templates/labels/fragments/label.html
+14
-2
appview/pages/templates/labels/fragments/label.html
···
1
1
{{ define "labels/fragments/label" }}
2
2
{{ $d := .def }}
3
3
{{ $v := .val }}
4
-
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 text-sm">
4
+
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
5
5
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
6
-
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ $v }}{{ end }}
6
+
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ template "labelVal" (dict "def" $d "val" $v) }}{{ end }}
7
7
</span>
8
8
{{ end }}
9
+
10
+
11
+
{{ define "labelVal" }}
12
+
{{ $d := .def }}
13
+
{{ $v := .val }}
14
+
15
+
{{ if $d.ValueType.IsDidFormat }}
16
+
{{ resolve $v }}
17
+
{{ else }}
18
+
{{ $v }}
19
+
{{ end }}
20
+
{{ end }}
+7
-1
appview/repo/repo.go
+7
-1
appview/repo/repo.go
···
987
987
// get form values for label definition
988
988
name := r.FormValue("name")
989
989
concreteType := r.FormValue("valueType")
990
+
valueFormat := r.FormValue("valueFormat")
990
991
enumValues := r.FormValue("enumValues")
991
992
scope := r.FormValue("scope")
992
993
color := r.FormValue("color")
···
999
1000
}
1000
1001
}
1001
1002
1003
+
format := db.ValueTypeFormatAny
1004
+
if valueFormat == "did" {
1005
+
format = db.ValueTypeFormatDid
1006
+
}
1007
+
1002
1008
valueType := db.ValueType{
1003
1009
Type: db.ConcreteType(concreteType),
1004
-
Format: db.ValueTypeFormatAny,
1010
+
Format: format,
1005
1011
Enum: variants,
1006
1012
}
1007
1013
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
···
78
78
cache := cache.New(config.Redis.Addr)
79
79
sess := session.New(cache)
80
80
oauth := oauth.NewOAuth(config, sess)
81
-
validator := validator.New(d)
81
+
validator := validator.New(d, res)
82
82
83
83
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
84
84
if err != nil {
+102
appview/validator/label.go
+102
appview/validator/label.go
···
1
1
package validator
2
2
3
3
import (
4
+
"context"
4
5
"fmt"
5
6
"regexp"
6
7
"strings"
···
75
76
76
77
return nil
77
78
}
79
+
80
+
func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
81
+
if labelDef == nil {
82
+
return fmt.Errorf("label definition is required")
83
+
}
84
+
if labelOp == nil {
85
+
return fmt.Errorf("label operation is required")
86
+
}
87
+
88
+
expectedKey := labelDef.AtUri().String()
89
+
if labelOp.OperandKey != expectedKey {
90
+
return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey)
91
+
}
92
+
93
+
if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel {
94
+
return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation)
95
+
}
96
+
97
+
if labelOp.Subject == "" {
98
+
return fmt.Errorf("subject URI is required")
99
+
}
100
+
if _, err := syntax.ParseATURI(string(labelOp.Subject)); err != nil {
101
+
return fmt.Errorf("invalid subject URI: %w", err)
102
+
}
103
+
104
+
if err := v.validateOperandValue(labelDef, labelOp); err != nil {
105
+
return fmt.Errorf("invalid operand value: %w", err)
106
+
}
107
+
108
+
// Validate performed time is not zero/invalid
109
+
if labelOp.PerformedAt.IsZero() {
110
+
return fmt.Errorf("performed_at timestamp is required")
111
+
}
112
+
113
+
return nil
114
+
}
115
+
116
+
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117
+
valueType := labelDef.ValueType
118
+
119
+
switch valueType.Type {
120
+
case db.ConcreteTypeNull:
121
+
// For null type, value should be empty
122
+
if labelOp.OperandValue != "null" {
123
+
return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue)
124
+
}
125
+
126
+
case db.ConcreteTypeString:
127
+
// For string type, validate enum constraints if present
128
+
if valueType.IsEnumType() {
129
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131
+
}
132
+
}
133
+
134
+
switch valueType.Format {
135
+
case db.ValueTypeFormatDid:
136
+
id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue)
137
+
if err != nil {
138
+
return fmt.Errorf("failed to resolve did/handle: %w", err)
139
+
}
140
+
141
+
labelOp.OperandValue = id.DID.String()
142
+
143
+
case db.ValueTypeFormatAny, "":
144
+
default:
145
+
return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
146
+
}
147
+
148
+
case db.ConcreteTypeInt:
149
+
if labelOp.OperandValue == "" {
150
+
return fmt.Errorf("integer type requires non-empty value")
151
+
}
152
+
if _, err := fmt.Sscanf(labelOp.OperandValue, "%d", new(int)); err != nil {
153
+
return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154
+
}
155
+
156
+
if valueType.IsEnumType() {
157
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159
+
}
160
+
}
161
+
162
+
case db.ConcreteTypeBool:
163
+
if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" {
164
+
return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue)
165
+
}
166
+
167
+
// validate enum constraints if present (though uncommon for booleans)
168
+
if valueType.IsEnumType() {
169
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171
+
}
172
+
}
173
+
174
+
default:
175
+
return fmt.Errorf("unsupported value type: %q", valueType.Type)
176
+
}
177
+
178
+
return nil
179
+
}
+4
-1
appview/validator/validator.go
+4
-1
appview/validator/validator.go
···
3
3
import (
4
4
"tangled.org/core/appview/db"
5
5
"tangled.org/core/appview/pages/markup"
6
+
"tangled.org/core/idresolver"
6
7
)
7
8
8
9
type Validator struct {
9
10
db *db.DB
10
11
sanitizer markup.Sanitizer
12
+
resolver *idresolver.Resolver
11
13
}
12
14
13
-
func New(db *db.DB) *Validator {
15
+
func New(db *db.DB, res *idresolver.Resolver) *Validator {
14
16
return &Validator{
15
17
db: db,
16
18
sanitizer: markup.NewSanitizer(),
19
+
resolver: res,
17
20
}
18
21
}