forked from tangled.org/core
this repo has no description

appview/models: move db.Label* into models

- db.{LabelOp,LabelDefinition,LabelState,LabelApplicationCtx} have been
moved
- auxilliary helpers used to calculate label state have been moved

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

oppi.li 6390396d 451ebd4a

verified
Changed files
+550 -537
appview
db
issues
labels
models
pages
repo
validator
+2 -1
appview/db/issues.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
)
···
// optionally, populate this when querying for reverse mappings
// like comment counts, parent repo etc.
Comments []IssueComment
-
Labels LabelState
+
Labels models.LabelState
Repo *Repo
}
+33 -496
appview/db/label.go
···
package db
import (
-
"crypto/sha1"
"database/sql"
-
"encoding/hex"
-
"errors"
"fmt"
"maps"
"slices"
···
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
-
"tangled.sh/tangled.sh/core/api/tangled"
-
"tangled.sh/tangled.sh/core/consts"
-
)
-
-
type ConcreteType string
-
-
const (
-
ConcreteTypeNull ConcreteType = "null"
-
ConcreteTypeString ConcreteType = "string"
-
ConcreteTypeInt ConcreteType = "integer"
-
ConcreteTypeBool ConcreteType = "boolean"
-
)
-
-
type ValueTypeFormat string
-
-
const (
-
ValueTypeFormatAny ValueTypeFormat = "any"
-
ValueTypeFormatDid ValueTypeFormat = "did"
+
"tangled.org/core/appview/models"
)
-
// ValueType represents an atproto lexicon type definition with constraints
-
type ValueType struct {
-
Type ConcreteType `json:"type"`
-
Format ValueTypeFormat `json:"format,omitempty"`
-
Enum []string `json:"enum,omitempty"`
-
}
-
-
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
-
return tangled.LabelDefinition_ValueType{
-
Type: string(vt.Type),
-
Format: string(vt.Format),
-
Enum: vt.Enum,
-
}
-
}
-
-
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
-
return ValueType{
-
Type: ConcreteType(record.Type),
-
Format: ValueTypeFormat(record.Format),
-
Enum: record.Enum,
-
}
-
}
-
-
func (vt ValueType) IsConcreteType() bool {
-
return vt.Type == ConcreteTypeNull ||
-
vt.Type == ConcreteTypeString ||
-
vt.Type == ConcreteTypeInt ||
-
vt.Type == ConcreteTypeBool
-
}
-
-
func (vt ValueType) IsNull() bool {
-
return vt.Type == ConcreteTypeNull
-
}
-
-
func (vt ValueType) IsString() bool {
-
return vt.Type == ConcreteTypeString
-
}
-
-
func (vt ValueType) IsInt() bool {
-
return vt.Type == ConcreteTypeInt
-
}
-
-
func (vt ValueType) IsBool() bool {
-
return vt.Type == ConcreteTypeBool
-
}
-
-
func (vt ValueType) IsEnum() 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 {
-
Id int64
-
Did string
-
Rkey string
-
-
Name string
-
ValueType ValueType
-
Scope []string
-
Color *string
-
Multiple bool
-
Created time.Time
-
}
-
-
func (l *LabelDefinition) AtUri() syntax.ATURI {
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
-
}
-
-
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
-
vt := l.ValueType.AsRecord()
-
return tangled.LabelDefinition{
-
Name: l.Name,
-
Color: l.Color,
-
CreatedAt: l.Created.Format(time.RFC3339),
-
Multiple: &l.Multiple,
-
Scope: l.Scope,
-
ValueType: &vt,
-
}
-
}
-
-
// random color for a given seed
-
func randomColor(seed string) string {
-
hash := sha1.Sum([]byte(seed))
-
hexStr := hex.EncodeToString(hash[:])
-
r := hexStr[0:2]
-
g := hexStr[2:4]
-
b := hexStr[4:6]
-
-
return fmt.Sprintf("#%s%s%s", r, g, b)
-
}
-
-
func (ld LabelDefinition) GetColor() string {
-
if ld.Color == nil {
-
seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
-
color := randomColor(seed)
-
return color
-
}
-
-
return *ld.Color
-
}
-
-
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
-
created, err := time.Parse(time.RFC3339, record.CreatedAt)
-
if err != nil {
-
created = time.Now()
-
}
-
-
multiple := false
-
if record.Multiple != nil {
-
multiple = *record.Multiple
-
}
-
-
var vt ValueType
-
if record.ValueType != nil {
-
vt = ValueTypeFromRecord(*record.ValueType)
-
}
-
-
return &LabelDefinition{
-
Did: did,
-
Rkey: rkey,
-
-
Name: record.Name,
-
ValueType: vt,
-
Scope: record.Scope,
-
Color: record.Color,
-
Multiple: multiple,
-
Created: created,
-
}, nil
-
}
-
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
-
var conditions []string
-
var args []any
-
for _, filter := range filters {
-
conditions = append(conditions, filter.Condition())
-
args = append(args, filter.Arg()...)
-
}
-
whereClause := ""
-
if conditions != nil {
-
whereClause = " where " + strings.Join(conditions, " and ")
-
}
-
query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
-
_, err := e.Exec(query, args...)
-
return err
-
}
-
// no updating type for now
-
func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) {
+
func AddLabelDefinition(e Execer, l *models.LabelDefinition) (int64, error) {
result, err := e.Exec(
`insert into label_definitions (
did,
···
return id, nil
}
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) {
-
var labelDefinitions []LabelDefinition
+
func DeleteLabelDefinition(e Execer, filters ...filter) error {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
+
_, err := e.Exec(query, args...)
+
return err
+
}
+
+
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
+
var labelDefinitions []models.LabelDefinition
var conditions []string
var args []any
···
defer rows.Close()
for rows.Next() {
-
var labelDefinition LabelDefinition
+
var labelDefinition models.LabelDefinition
var createdAt, enumVariants, scopes string
var color sql.Null[string]
var multiple int
···
}
// helper to get exactly one label def
-
func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) {
+
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
labels, err := GetLabelDefinitions(e, filters...)
if err != nil {
return nil, err
···
return &labels[0], nil
}
-
type LabelOp struct {
-
Id int64
-
Did string
-
Rkey string
-
Subject syntax.ATURI
-
Operation LabelOperation
-
OperandKey string
-
OperandValue string
-
PerformedAt time.Time
-
IndexedAt time.Time
-
}
-
-
func (l LabelOp) SortAt() time.Time {
-
createdAt := l.PerformedAt
-
indexedAt := l.IndexedAt
-
-
// if we don't have an indexedat, fall back to now
-
if indexedAt.IsZero() {
-
indexedAt = time.Now()
-
}
-
-
// if createdat is invalid (before epoch), treat as null -> return zero time
-
if createdAt.Before(time.UnixMicro(0)) {
-
return time.Time{}
-
}
-
-
// if createdat is <= indexedat, use createdat
-
if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
-
return createdAt
-
}
-
-
// otherwise, createdat is in the future relative to indexedat -> use indexedat
-
return indexedAt
-
}
-
-
type LabelOperation string
-
-
const (
-
LabelOperationAdd LabelOperation = "add"
-
LabelOperationDel LabelOperation = "del"
-
)
-
-
// a record can create multiple label ops
-
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
-
performed, err := time.Parse(time.RFC3339, record.PerformedAt)
-
if err != nil {
-
performed = time.Now()
-
}
-
-
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
-
return LabelOp{
-
Did: did,
-
Rkey: rkey,
-
Subject: syntax.ATURI(record.Subject),
-
OperandKey: operand.Key,
-
OperandValue: operand.Value,
-
PerformedAt: performed,
-
}
-
}
-
-
var ops []LabelOp
-
for _, o := range record.Add {
-
if o != nil {
-
op := mkOp(o)
-
op.Operation = LabelOperationAdd
-
ops = append(ops, op)
-
}
-
}
-
for _, o := range record.Delete {
-
if o != nil {
-
op := mkOp(o)
-
op.Operation = LabelOperationDel
-
ops = append(ops, op)
-
}
-
}
-
-
return ops
-
}
-
-
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
-
if len(ops) == 0 {
-
return tangled.LabelOp{}
-
}
-
-
// use the first operation to establish common fields
-
first := ops[0]
-
record := tangled.LabelOp{
-
Subject: string(first.Subject),
-
PerformedAt: first.PerformedAt.Format(time.RFC3339),
-
}
-
-
var addOperands []*tangled.LabelOp_Operand
-
var deleteOperands []*tangled.LabelOp_Operand
-
-
for _, op := range ops {
-
operand := &tangled.LabelOp_Operand{
-
Key: op.OperandKey,
-
Value: op.OperandValue,
-
}
-
-
switch op.Operation {
-
case LabelOperationAdd:
-
addOperands = append(addOperands, operand)
-
case LabelOperationDel:
-
deleteOperands = append(deleteOperands, operand)
-
default:
-
return tangled.LabelOp{}
-
}
-
}
-
-
record.Add = addOperands
-
record.Delete = deleteOperands
-
-
return record
-
}
-
-
func AddLabelOp(e Execer, l *LabelOp) (int64, error) {
+
func AddLabelOp(e Execer, l *models.LabelOp) (int64, error) {
now := time.Now()
result, err := e.Exec(
`insert into label_ops (
···
return id, nil
}
-
func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) {
-
var labelOps []LabelOp
+
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
+
var labelOps []models.LabelOp
var conditions []string
var args []any
···
defer rows.Close()
for rows.Next() {
-
var labelOp LabelOp
+
var labelOp models.LabelOp
var performedAt, indexedAt string
if err := rows.Scan(
···
}
// get labels for a given list of subject URIs
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) {
+
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
ops, err := GetLabelOps(e, filters...)
if err != nil {
return nil, err
}
// group ops by subject
-
opsBySubject := make(map[syntax.ATURI][]LabelOp)
+
opsBySubject := make(map[syntax.ATURI][]models.LabelOp)
for _, op := range ops {
subject := syntax.ATURI(op.Subject)
opsBySubject[subject] = append(opsBySubject[subject], op)
···
}
// apply label ops for each subject and collect results
-
results := make(map[syntax.ATURI]LabelState)
+
results := make(map[syntax.ATURI]models.LabelState)
for subject, subjectOps := range opsBySubject {
-
state := NewLabelState()
+
state := models.NewLabelState()
actx.ApplyLabelOps(state, subjectOps)
results[subject] = state
}
···
return results, nil
}
-
type set = map[string]struct{}
-
-
type LabelState struct {
-
inner map[string]set
-
}
-
-
func NewLabelState() LabelState {
-
return LabelState{
-
inner: make(map[string]set),
-
}
-
}
-
-
func (s LabelState) Inner() map[string]set {
-
return s.inner
-
}
-
-
func (s LabelState) ContainsLabel(l string) bool {
-
if valset, exists := s.inner[l]; exists {
-
if valset != nil {
-
return true
-
}
-
}
-
-
return false
-
}
-
-
// go maps behavior in templates make this necessary,
-
// indexing a map and getting `set` in return is apparently truthy
-
func (s LabelState) ContainsLabelAndVal(l, v string) bool {
-
if valset, exists := s.inner[l]; exists {
-
if _, exists := valset[v]; exists {
-
return true
-
}
-
}
-
-
return false
-
}
-
-
func (s LabelState) GetValSet(l string) set {
-
if valset, exists := s.inner[l]; exists {
-
return valset
-
} else {
-
return make(set)
-
}
-
}
-
-
type LabelApplicationCtx struct {
-
Defs map[string]*LabelDefinition // labelAt -> labelDef
-
}
-
-
var (
-
LabelNoOpError = errors.New("no-op")
-
)
-
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) {
+
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
labels, err := GetLabelDefinitions(e, filters...)
if err != nil {
return nil, err
}
-
defs := make(map[string]*LabelDefinition)
+
defs := make(map[string]*models.LabelDefinition)
for _, l := range labels {
defs[l.AtUri().String()] = &l
}
-
return &LabelApplicationCtx{defs}, nil
-
}
-
-
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
-
def, ok := c.Defs[op.OperandKey]
-
if !ok {
-
// this def was deleted, but an op exists, so we just skip over the op
-
return nil
-
}
-
-
switch op.Operation {
-
case LabelOperationAdd:
-
// if valueset is empty, init it
-
if state.inner[op.OperandKey] == nil {
-
state.inner[op.OperandKey] = make(set)
-
}
-
-
// if valueset is populated & this val alr exists, this labelop is a noop
-
if valueSet, exists := state.inner[op.OperandKey]; exists {
-
if _, exists = valueSet[op.OperandValue]; exists {
-
return LabelNoOpError
-
}
-
}
-
-
if def.Multiple {
-
// append to set
-
state.inner[op.OperandKey][op.OperandValue] = struct{}{}
-
} else {
-
// reset to just this value
-
state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
-
}
-
-
case LabelOperationDel:
-
// if label DNE, then deletion is a no-op
-
if valueSet, exists := state.inner[op.OperandKey]; !exists {
-
return LabelNoOpError
-
} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
-
return LabelNoOpError
-
}
-
-
if def.Multiple {
-
// remove from set
-
delete(state.inner[op.OperandKey], op.OperandValue)
-
} else {
-
// reset the entire label
-
delete(state.inner, op.OperandKey)
-
}
-
-
// if the map becomes empty, then set it to nil, this is just the inverse of add
-
if len(state.inner[op.OperandKey]) == 0 {
-
state.inner[op.OperandKey] = nil
-
}
-
-
}
-
-
return nil
-
}
-
-
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
-
// sort label ops in sort order first
-
slices.SortFunc(ops, func(a, b LabelOp) int {
-
return a.SortAt().Compare(b.SortAt())
-
})
-
-
// apply ops in sequence
-
for _, o := range ops {
-
_ = c.ApplyLabelOp(state, o)
-
}
-
}
-
-
// IsInverse checks if one label operation is the inverse of another
-
// returns true if one is an add and the other is a delete with the same key and value
-
func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
-
if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
-
return false
-
}
-
-
return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
-
(op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
-
}
-
-
// removes pairs of label operations that are inverses of each other
-
// from the given slice. the function preserves the order of remaining operations.
-
func ReduceLabelOps(ops []LabelOp) []LabelOp {
-
if len(ops) <= 1 {
-
return ops
-
}
-
-
keep := make([]bool, len(ops))
-
for i := range keep {
-
keep[i] = true
-
}
-
-
for i := range ops {
-
if !keep[i] {
-
continue
-
}
-
-
for j := i + 1; j < len(ops); j++ {
-
if !keep[j] {
-
continue
-
}
-
-
if ops[i].IsInverse(ops[j]) {
-
keep[i] = false
-
keep[j] = false
-
break // move to next i since this one is now eliminated
-
}
-
}
-
}
-
-
// build result slice with only kept operations
-
var result []LabelOp
-
for i, op := range ops {
-
if keep[i] {
-
result = append(result, op)
-
}
-
}
-
-
return result
-
}
-
-
func DefaultLabelDefs() []string {
-
rkeys := []string{
-
"wontfix",
-
"duplicate",
-
"assignee",
-
"good-first-issue",
-
"documentation",
-
}
-
-
defs := make([]string, len(rkeys))
-
for i, r := range rkeys {
-
defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
-
}
-
-
return defs
+
return &models.LabelApplicationCtx{defs}, nil
}
+1 -1
appview/ingester.go
···
return fmt.Errorf("invalid record: %w", err)
}
-
def, err := db.LabelDefinitionFromRecord(did, rkey, record)
+
def, err := models.LabelDefinitionFromRecord(did, rkey, record)
if err != nil {
return fmt.Errorf("failed to parse labeldef from record: %w", err)
}
+3 -2
appview/issues/issues.go
···
"tangled.org/core/api/tangled"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
···
return
}
-
defs := make(map[string]*db.LabelDefinition)
+
defs := make(map[string]*models.LabelDefinition)
for _, l := range labelDefs {
defs[l.AtUri().String()] = &l
}
···
return
}
-
defs := make(map[string]*db.LabelDefinition)
+
defs := make(map[string]*models.LabelDefinition)
for _, l := range labelDefs {
defs[l.AtUri().String()] = &l
}
+10 -9
appview/labels/labels.go
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/middleware"
+
"tangled.sh/tangled.sh/core/appview/models"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/validator"
···
return
}
-
labelState := db.NewLabelState()
+
labelState := models.NewLabelState()
actx.ApplyLabelOps(labelState, existingOps)
-
var labelOps []db.LabelOp
+
var labelOps []models.LabelOp
// first delete all existing state
for key, vals := range labelState.Inner() {
for val := range vals {
-
labelOps = append(labelOps, db.LabelOp{
+
labelOps = append(labelOps, models.LabelOp{
Did: did,
Rkey: rkey,
Subject: syntax.ATURI(subjectUri),
-
Operation: db.LabelOperationDel,
+
Operation: models.LabelOperationDel,
OperandKey: key,
OperandValue: val,
PerformedAt: performedAt,
···
}
for _, val := range vals {
-
labelOps = append(labelOps, db.LabelOp{
+
labelOps = append(labelOps, models.LabelOp{
Did: did,
Rkey: rkey,
Subject: syntax.ATURI(subjectUri),
-
Operation: db.LabelOperationAdd,
+
Operation: models.LabelOperationAdd,
OperandKey: key,
OperandValue: val,
PerformedAt: performedAt,
···
}
// reduce the opset
-
labelOps = db.ReduceLabelOps(labelOps)
+
labelOps = models.ReduceLabelOps(labelOps)
for i := range labelOps {
def := actx.Defs[labelOps[i].OperandKey]
···
// next, apply all ops introduced in this request and filter out ones that are no-ops
validLabelOps := labelOps[:0]
for _, op := range labelOps {
-
if err = actx.ApplyLabelOp(labelState, op); err != db.LabelNoOpError {
+
if err = actx.ApplyLabelOp(labelState, op); err != models.LabelNoOpError {
validLabelOps = append(validLabelOps, op)
}
}
···
}
// create an atproto record of valid ops
-
record := db.LabelOpsAsRecord(validLabelOps)
+
record := models.LabelOpsAsRecord(validLabelOps)
client, err := l.oauth.AuthorizedClient(r)
if err != nil {
+473
appview/models/label.go
···
+
package models
+
+
import (
+
"crypto/sha1"
+
"encoding/hex"
+
"errors"
+
"fmt"
+
"slices"
+
"time"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/consts"
+
)
+
+
type ConcreteType string
+
+
const (
+
ConcreteTypeNull ConcreteType = "null"
+
ConcreteTypeString ConcreteType = "string"
+
ConcreteTypeInt ConcreteType = "integer"
+
ConcreteTypeBool ConcreteType = "boolean"
+
)
+
+
type ValueTypeFormat string
+
+
const (
+
ValueTypeFormatAny ValueTypeFormat = "any"
+
ValueTypeFormatDid ValueTypeFormat = "did"
+
)
+
+
// ValueType represents an atproto lexicon type definition with constraints
+
type ValueType struct {
+
Type ConcreteType `json:"type"`
+
Format ValueTypeFormat `json:"format,omitempty"`
+
Enum []string `json:"enum,omitempty"`
+
}
+
+
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
+
return tangled.LabelDefinition_ValueType{
+
Type: string(vt.Type),
+
Format: string(vt.Format),
+
Enum: vt.Enum,
+
}
+
}
+
+
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
+
return ValueType{
+
Type: ConcreteType(record.Type),
+
Format: ValueTypeFormat(record.Format),
+
Enum: record.Enum,
+
}
+
}
+
+
func (vt ValueType) IsConcreteType() bool {
+
return vt.Type == ConcreteTypeNull ||
+
vt.Type == ConcreteTypeString ||
+
vt.Type == ConcreteTypeInt ||
+
vt.Type == ConcreteTypeBool
+
}
+
+
func (vt ValueType) IsNull() bool {
+
return vt.Type == ConcreteTypeNull
+
}
+
+
func (vt ValueType) IsString() bool {
+
return vt.Type == ConcreteTypeString
+
}
+
+
func (vt ValueType) IsInt() bool {
+
return vt.Type == ConcreteTypeInt
+
}
+
+
func (vt ValueType) IsBool() bool {
+
return vt.Type == ConcreteTypeBool
+
}
+
+
func (vt ValueType) IsEnum() 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 {
+
Id int64
+
Did string
+
Rkey string
+
+
Name string
+
ValueType ValueType
+
Scope []string
+
Color *string
+
Multiple bool
+
Created time.Time
+
}
+
+
func (l *LabelDefinition) AtUri() syntax.ATURI {
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
+
}
+
+
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
+
vt := l.ValueType.AsRecord()
+
return tangled.LabelDefinition{
+
Name: l.Name,
+
Color: l.Color,
+
CreatedAt: l.Created.Format(time.RFC3339),
+
Multiple: &l.Multiple,
+
Scope: l.Scope,
+
ValueType: &vt,
+
}
+
}
+
+
// random color for a given seed
+
func randomColor(seed string) string {
+
hash := sha1.Sum([]byte(seed))
+
hexStr := hex.EncodeToString(hash[:])
+
r := hexStr[0:2]
+
g := hexStr[2:4]
+
b := hexStr[4:6]
+
+
return fmt.Sprintf("#%s%s%s", r, g, b)
+
}
+
+
func (ld LabelDefinition) GetColor() string {
+
if ld.Color == nil {
+
seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
+
color := randomColor(seed)
+
return color
+
}
+
+
return *ld.Color
+
}
+
+
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
+
if err != nil {
+
created = time.Now()
+
}
+
+
multiple := false
+
if record.Multiple != nil {
+
multiple = *record.Multiple
+
}
+
+
var vt ValueType
+
if record.ValueType != nil {
+
vt = ValueTypeFromRecord(*record.ValueType)
+
}
+
+
return &LabelDefinition{
+
Did: did,
+
Rkey: rkey,
+
+
Name: record.Name,
+
ValueType: vt,
+
Scope: record.Scope,
+
Color: record.Color,
+
Multiple: multiple,
+
Created: created,
+
}, nil
+
}
+
+
type LabelOp struct {
+
Id int64
+
Did string
+
Rkey string
+
Subject syntax.ATURI
+
Operation LabelOperation
+
OperandKey string
+
OperandValue string
+
PerformedAt time.Time
+
IndexedAt time.Time
+
}
+
+
func (l LabelOp) SortAt() time.Time {
+
createdAt := l.PerformedAt
+
indexedAt := l.IndexedAt
+
+
// if we don't have an indexedat, fall back to now
+
if indexedAt.IsZero() {
+
indexedAt = time.Now()
+
}
+
+
// if createdat is invalid (before epoch), treat as null -> return zero time
+
if createdAt.Before(time.UnixMicro(0)) {
+
return time.Time{}
+
}
+
+
// if createdat is <= indexedat, use createdat
+
if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
+
return createdAt
+
}
+
+
// otherwise, createdat is in the future relative to indexedat -> use indexedat
+
return indexedAt
+
}
+
+
type LabelOperation string
+
+
const (
+
LabelOperationAdd LabelOperation = "add"
+
LabelOperationDel LabelOperation = "del"
+
)
+
+
// a record can create multiple label ops
+
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
+
performed, err := time.Parse(time.RFC3339, record.PerformedAt)
+
if err != nil {
+
performed = time.Now()
+
}
+
+
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
+
return LabelOp{
+
Did: did,
+
Rkey: rkey,
+
Subject: syntax.ATURI(record.Subject),
+
OperandKey: operand.Key,
+
OperandValue: operand.Value,
+
PerformedAt: performed,
+
}
+
}
+
+
var ops []LabelOp
+
for _, o := range record.Add {
+
if o != nil {
+
op := mkOp(o)
+
op.Operation = LabelOperationAdd
+
ops = append(ops, op)
+
}
+
}
+
for _, o := range record.Delete {
+
if o != nil {
+
op := mkOp(o)
+
op.Operation = LabelOperationDel
+
ops = append(ops, op)
+
}
+
}
+
+
return ops
+
}
+
+
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
+
if len(ops) == 0 {
+
return tangled.LabelOp{}
+
}
+
+
// use the first operation to establish common fields
+
first := ops[0]
+
record := tangled.LabelOp{
+
Subject: string(first.Subject),
+
PerformedAt: first.PerformedAt.Format(time.RFC3339),
+
}
+
+
var addOperands []*tangled.LabelOp_Operand
+
var deleteOperands []*tangled.LabelOp_Operand
+
+
for _, op := range ops {
+
operand := &tangled.LabelOp_Operand{
+
Key: op.OperandKey,
+
Value: op.OperandValue,
+
}
+
+
switch op.Operation {
+
case LabelOperationAdd:
+
addOperands = append(addOperands, operand)
+
case LabelOperationDel:
+
deleteOperands = append(deleteOperands, operand)
+
default:
+
return tangled.LabelOp{}
+
}
+
}
+
+
record.Add = addOperands
+
record.Delete = deleteOperands
+
+
return record
+
}
+
+
type set = map[string]struct{}
+
+
type LabelState struct {
+
inner map[string]set
+
}
+
+
func NewLabelState() LabelState {
+
return LabelState{
+
inner: make(map[string]set),
+
}
+
}
+
+
func (s LabelState) Inner() map[string]set {
+
return s.inner
+
}
+
+
func (s LabelState) ContainsLabel(l string) bool {
+
if valset, exists := s.inner[l]; exists {
+
if valset != nil {
+
return true
+
}
+
}
+
+
return false
+
}
+
+
// go maps behavior in templates make this necessary,
+
// indexing a map and getting `set` in return is apparently truthy
+
func (s LabelState) ContainsLabelAndVal(l, v string) bool {
+
if valset, exists := s.inner[l]; exists {
+
if _, exists := valset[v]; exists {
+
return true
+
}
+
}
+
+
return false
+
}
+
+
func (s LabelState) GetValSet(l string) set {
+
if valset, exists := s.inner[l]; exists {
+
return valset
+
} else {
+
return make(set)
+
}
+
}
+
+
type LabelApplicationCtx struct {
+
Defs map[string]*LabelDefinition // labelAt -> labelDef
+
}
+
+
var (
+
LabelNoOpError = errors.New("no-op")
+
)
+
+
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
+
def, ok := c.Defs[op.OperandKey]
+
if !ok {
+
// this def was deleted, but an op exists, so we just skip over the op
+
return nil
+
}
+
+
switch op.Operation {
+
case LabelOperationAdd:
+
// if valueset is empty, init it
+
if state.inner[op.OperandKey] == nil {
+
state.inner[op.OperandKey] = make(set)
+
}
+
+
// if valueset is populated & this val alr exists, this labelop is a noop
+
if valueSet, exists := state.inner[op.OperandKey]; exists {
+
if _, exists = valueSet[op.OperandValue]; exists {
+
return LabelNoOpError
+
}
+
}
+
+
if def.Multiple {
+
// append to set
+
state.inner[op.OperandKey][op.OperandValue] = struct{}{}
+
} else {
+
// reset to just this value
+
state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
+
}
+
+
case LabelOperationDel:
+
// if label DNE, then deletion is a no-op
+
if valueSet, exists := state.inner[op.OperandKey]; !exists {
+
return LabelNoOpError
+
} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
+
return LabelNoOpError
+
}
+
+
if def.Multiple {
+
// remove from set
+
delete(state.inner[op.OperandKey], op.OperandValue)
+
} else {
+
// reset the entire label
+
delete(state.inner, op.OperandKey)
+
}
+
+
// if the map becomes empty, then set it to nil, this is just the inverse of add
+
if len(state.inner[op.OperandKey]) == 0 {
+
state.inner[op.OperandKey] = nil
+
}
+
+
}
+
+
return nil
+
}
+
+
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
+
// sort label ops in sort order first
+
slices.SortFunc(ops, func(a, b LabelOp) int {
+
return a.SortAt().Compare(b.SortAt())
+
})
+
+
// apply ops in sequence
+
for _, o := range ops {
+
_ = c.ApplyLabelOp(state, o)
+
}
+
}
+
+
// IsInverse checks if one label operation is the inverse of another
+
// returns true if one is an add and the other is a delete with the same key and value
+
func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
+
if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
+
return false
+
}
+
+
return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
+
(op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
+
}
+
+
// removes pairs of label operations that are inverses of each other
+
// from the given slice. the function preserves the order of remaining operations.
+
func ReduceLabelOps(ops []LabelOp) []LabelOp {
+
if len(ops) <= 1 {
+
return ops
+
}
+
+
keep := make([]bool, len(ops))
+
for i := range keep {
+
keep[i] = true
+
}
+
+
for i := range ops {
+
if !keep[i] {
+
continue
+
}
+
+
for j := i + 1; j < len(ops); j++ {
+
if !keep[j] {
+
continue
+
}
+
+
if ops[i].IsInverse(ops[j]) {
+
keep[i] = false
+
keep[j] = false
+
break // move to next i since this one is now eliminated
+
}
+
}
+
}
+
+
// build result slice with only kept operations
+
var result []LabelOp
+
for i, op := range ops {
+
if keep[i] {
+
result = append(result, op)
+
}
+
}
+
+
return result
+
}
+
+
func DefaultLabelDefs() []string {
+
rkeys := []string{
+
"wontfix",
+
"duplicate",
+
"assignee",
+
"good-first-issue",
+
"documentation",
+
}
+
+
defs := make([]string, len(rkeys))
+
for i, r := range rkeys {
+
defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
+
}
+
+
return defs
+
}
+8 -8
appview/pages/pages.go
···
type RepoGeneralSettingsParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Labels []db.LabelDefinition
-
DefaultLabels []db.LabelDefinition
+
Labels []models.LabelDefinition
+
DefaultLabels []models.LabelDefinition
SubscribedLabels map[string]struct{}
Active string
Tabs []map[string]any
···
RepoInfo repoinfo.RepoInfo
Active string
Issues []db.Issue
-
LabelDefs map[string]*db.LabelDefinition
+
LabelDefs map[string]*models.LabelDefinition
Page pagination.Page
FilteringByOpen bool
}
···
Active string
Issue *db.Issue
CommentList []db.CommentListItem
-
LabelDefs map[string]*db.LabelDefinition
+
LabelDefs map[string]*models.LabelDefinition
OrderedReactionKinds []db.ReactionKind
Reactions map[db.ReactionKind]int
···
type LabelPanelParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Defs map[string]*db.LabelDefinition
+
Defs map[string]*models.LabelDefinition
Subject string
-
State db.LabelState
+
State models.LabelState
func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
···
type EditLabelPanelParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Defs map[string]*db.LabelDefinition
+
Defs map[string]*models.LabelDefinition
Subject string
-
State db.LabelState
+
State models.LabelState
func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
+8 -8
appview/repo/repo.go
···
concreteType = "null"
-
format := db.ValueTypeFormatAny
+
format := models.ValueTypeFormatAny
if valueFormat == "did" {
-
format = db.ValueTypeFormatDid
+
format = models.ValueTypeFormatDid
-
valueType := db.ValueType{
-
Type: db.ConcreteType(concreteType),
+
valueType := models.ValueType{
+
Type: models.ConcreteType(concreteType),
Format: format,
Enum: variants,
-
label := db.LabelDefinition{
+
label := models.LabelDefinition{
Did: user.Did,
Rkey: tid.TID(),
Name: name,
···
return
-
defs := make(map[string]*db.LabelDefinition)
+
defs := make(map[string]*models.LabelDefinition)
for _, l := range labelDefs {
defs[l.AtUri().String()] = &l
···
return
-
defs := make(map[string]*db.LabelDefinition)
+
defs := make(map[string]*models.LabelDefinition)
for _, l := range labelDefs {
defs[l.AtUri().String()] = &l
···
return
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs()))
if err != nil {
log.Println("failed to fetch labels", err)
rp.pages.Error503(w)
+12 -12
appview/validator/label.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"golang.org/x/exp/slices"
"tangled.sh/tangled.sh/core/api/tangled"
-
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/models"
)
var (
···
validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
)
-
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
+
func (v *Validator) ValidateLabelDefinition(label *models.LabelDefinition) error {
if label.Name == "" {
return fmt.Errorf("label name is empty")
}
···
return nil
}
-
func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
+
func (v *Validator) ValidateLabelOp(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error {
if labelDef == nil {
return fmt.Errorf("label definition is required")
}
···
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 {
+
if labelOp.Operation != models.LabelOperationAdd && labelOp.Operation != models.LabelOperationDel {
return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation)
}
···
return nil
}
-
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
+
func (v *Validator) validateOperandValue(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error {
valueType := labelDef.ValueType
// this is permitted, it "unsets" a label
if labelOp.OperandValue == "" {
-
labelOp.Operation = db.LabelOperationDel
+
labelOp.Operation = models.LabelOperationDel
return nil
}
switch valueType.Type {
-
case db.ConcreteTypeNull:
+
case models.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:
+
case models.ConcreteTypeString:
// For string type, validate enum constraints if present
if valueType.IsEnum() {
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
···
}
switch valueType.Format {
-
case db.ValueTypeFormatDid:
+
case models.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, "":
+
case models.ValueTypeFormatAny, "":
default:
return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
}
-
case db.ConcreteTypeInt:
+
case models.ConcreteTypeInt:
if labelOp.OperandValue == "" {
return fmt.Errorf("integer type requires non-empty value")
}
···
}
}
-
case db.ConcreteTypeBool:
+
case models.ConcreteTypeBool:
if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" {
return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue)
}