forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/db: support DID formats in labels

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

oppi.li a20273b1 30d72737

verified
Changed files
+143 -16
appview
db
issues
pages
templates
labels
fragments
repo
state
validator
+10 -10
appview/db/label.go
···
"encoding/hex"
"errors"
"fmt"
-
"log"
"maps"
"slices"
"strings"
···
func (vt ValueType) IsEnumType() bool {
return len(vt.Enum) > 0
+
}
+
+
func (vt ValueType) IsDidFormat() bool {
+
return vt.Format == ValueTypeFormatDid
+
}
+
+
func (vt ValueType) IsAnyFormat() bool {
+
return vt.Format == ValueTypeFormatAny
}
type LabelDefinition struct {
···
results[subject] = state
}
-
log.Println("results for get labels", "s", results)
-
return results, nil
}
···
}
type LabelApplicationCtx struct {
-
defs map[string]*LabelDefinition // labelAt -> labelDef
+
Defs map[string]*LabelDefinition // labelAt -> labelDef
}
var (
···
}
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
-
def := c.defs[op.OperandKey]
+
def := c.Defs[op.OperandKey]
switch op.Operation {
case LabelOperationAdd:
···
_ = c.ApplyLabelOp(state, o)
}
}
-
-
type Label struct {
-
def *LabelDefinition
-
val set
-
}
+5 -1
appview/issues/issues.go
···
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
}
-
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
labelDefs, err := db.GetLabelDefinitions(
+
rp.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterEq("scope", tangled.RepoIssueNSID),
+
)
if err != nil {
log.Println("failed to fetch labels", err)
rp.pages.Error503(w)
+14 -2
appview/pages/templates/labels/fragments/label.html
···
{{ define "labels/fragments/label" }}
{{ $d := .def }}
{{ $v := .val }}
-
<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">
+
<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">
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
-
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ $v }}{{ end }}
+
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ template "labelVal" (dict "def" $d "val" $v) }}{{ end }}
</span>
{{ end }}
+
+
+
{{ define "labelVal" }}
+
{{ $d := .def }}
+
{{ $v := .val }}
+
+
{{ if $d.ValueType.IsDidFormat }}
+
{{ resolve $v }}
+
{{ else }}
+
{{ $v }}
+
{{ end }}
+
{{ end }}
+7 -1
appview/repo/repo.go
···
// get form values for label definition
name := r.FormValue("name")
concreteType := r.FormValue("valueType")
+
valueFormat := r.FormValue("valueFormat")
enumValues := r.FormValue("enumValues")
scope := r.FormValue("scope")
color := r.FormValue("color")
···
}
+
format := db.ValueTypeFormatAny
+
if valueFormat == "did" {
+
format = db.ValueTypeFormatDid
+
}
+
valueType := db.ValueType{
Type: db.ConcreteType(concreteType),
-
Format: db.ValueTypeFormatAny,
+
Format: format,
Enum: variants,
+1 -1
appview/state/state.go
···
cache := cache.New(config.Redis.Addr)
sess := session.New(cache)
oauth := oauth.NewOAuth(config, sess)
-
validator := validator.New(d)
+
validator := validator.New(d, res)
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
if err != nil {
+102
appview/validator/label.go
···
package validator
import (
+
"context"
"fmt"
"regexp"
"strings"
···
return nil
}
+
+
func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
+
if labelDef == nil {
+
return fmt.Errorf("label definition is required")
+
}
+
if labelOp == nil {
+
return fmt.Errorf("label operation is required")
+
}
+
+
expectedKey := labelDef.AtUri().String()
+
if labelOp.OperandKey != expectedKey {
+
return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey)
+
}
+
+
if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel {
+
return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation)
+
}
+
+
if labelOp.Subject == "" {
+
return fmt.Errorf("subject URI is required")
+
}
+
if _, err := syntax.ParseATURI(string(labelOp.Subject)); err != nil {
+
return fmt.Errorf("invalid subject URI: %w", err)
+
}
+
+
if err := v.validateOperandValue(labelDef, labelOp); err != nil {
+
return fmt.Errorf("invalid operand value: %w", err)
+
}
+
+
// Validate performed time is not zero/invalid
+
if labelOp.PerformedAt.IsZero() {
+
return fmt.Errorf("performed_at timestamp is required")
+
}
+
+
return nil
+
}
+
+
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
+
valueType := labelDef.ValueType
+
+
switch valueType.Type {
+
case db.ConcreteTypeNull:
+
// For null type, value should be empty
+
if labelOp.OperandValue != "null" {
+
return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue)
+
}
+
+
case db.ConcreteTypeString:
+
// For string type, validate enum constraints if present
+
if valueType.IsEnumType() {
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
+
}
+
}
+
+
switch valueType.Format {
+
case db.ValueTypeFormatDid:
+
id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue)
+
if err != nil {
+
return fmt.Errorf("failed to resolve did/handle: %w", err)
+
}
+
+
labelOp.OperandValue = id.DID.String()
+
+
case db.ValueTypeFormatAny, "":
+
default:
+
return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
+
}
+
+
case db.ConcreteTypeInt:
+
if labelOp.OperandValue == "" {
+
return fmt.Errorf("integer type requires non-empty value")
+
}
+
if _, err := fmt.Sscanf(labelOp.OperandValue, "%d", new(int)); err != nil {
+
return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
+
}
+
+
if valueType.IsEnumType() {
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
+
}
+
}
+
+
case db.ConcreteTypeBool:
+
if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" {
+
return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue)
+
}
+
+
// validate enum constraints if present (though uncommon for booleans)
+
if valueType.IsEnumType() {
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
+
}
+
}
+
+
default:
+
return fmt.Errorf("unsupported value type: %q", valueType.Type)
+
}
+
+
return nil
+
}
+4 -1
appview/validator/validator.go
···
import (
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages/markup"
+
"tangled.sh/tangled.sh/core/idresolver"
)
type Validator struct {
db *db.DB
sanitizer markup.Sanitizer
+
resolver *idresolver.Resolver
}
-
func New(db *db.DB) *Validator {
+
func New(db *db.DB, res *idresolver.Resolver) *Validator {
return &Validator{
db: db,
sanitizer: markup.NewSanitizer(),
+
resolver: res,
}
}