Monorepo for Tangled tangled.org

appview/db: support DID formats in labels

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 5eb184ad 2ca97784

verified
Changed files
+143 -16
appview
db
issues
pages
templates
labels
fragments
repo
state
validator
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 }