+13
-8
appview/db/db.go
+13
-8
appview/db/db.go
···
483
)),
484
value_format text not null default "any",
485
value_enum text, -- comma separated list
486
-
scope text not null,
487
color text,
488
multiple integer not null default 0,
489
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
···
996
}
997
}
998
999
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1000
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1001
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1002
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1003
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1004
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1005
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1006
1007
func (f filter) Condition() string {
1008
rv := reflect.ValueOf(f.arg)
···
483
)),
484
value_format text not null default "any",
485
value_enum text, -- comma separated list
486
+
scope text not null, -- comma separated list of nsid
487
color text,
488
multiple integer not null default 0,
489
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
···
996
}
997
}
998
999
+
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1000
+
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1001
+
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1002
+
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1003
+
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1004
+
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1005
+
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1006
+
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
1007
+
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
1008
+
func FilterContains(key string, arg any) filter {
1009
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
1010
+
}
1011
1012
func (f filter) Condition() string {
1013
rv := reflect.ValueOf(f.arg)
+1
-1
appview/issues/issues.go
+1
-1
appview/issues/issues.go
+145
-14
appview/repo/repo.go
+145
-14
appview/repo/repo.go
···
966
rp.pages.HxRefresh(w)
967
}
968
969
-
func (rp *Repo) AddLabel(w http.ResponseWriter, r *http.Request) {
970
user := rp.oauth.GetUser(r)
971
l := rp.logger.With("handler", "AddLabel")
972
l = l.With("did", user.Did)
···
989
concreteType := r.FormValue("valueType")
990
valueFormat := r.FormValue("valueFormat")
991
enumValues := r.FormValue("enumValues")
992
-
scope := r.FormValue("scope")
993
color := r.FormValue("color")
994
multiple := r.FormValue("multiple") == "true"
995
···
1000
}
1001
}
1002
1003
format := db.ValueTypeFormatAny
1004
if valueFormat == "did" {
1005
format = db.ValueTypeFormatDid
···
1016
Rkey: tid.TID(),
1017
Name: name,
1018
ValueType: valueType,
1019
-
Scope: syntax.NSID(scope),
1020
Color: &color,
1021
Multiple: multiple,
1022
Created: time.Now(),
···
1072
Val: &repoRecord,
1073
},
1074
})
1075
1076
tx, err := rp.db.BeginTx(r.Context(), nil)
1077
if err != nil {
···
1118
rp.pages.HxRefresh(w)
1119
}
1120
1121
-
func (rp *Repo) DeleteLabel(w http.ResponseWriter, r *http.Request) {
1122
user := rp.oauth.GetUser(r)
1123
l := rp.logger.With("handler", "DeleteLabel")
1124
l = l.With("did", user.Did)
···
1229
1230
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
1231
user := rp.oauth.GetUser(r)
1232
-
l := rp.logger.With("handler", "DeleteLabel")
1233
l = l.With("did", user.Did)
1234
l = l.With("handle", user.Handle)
1235
···
1239
return
1240
}
1241
1242
-
errorId := "label-operation"
1243
fail := func(msg string, err error) {
1244
l.Error(msg, "err", err)
1245
rp.pages.Notice(w, errorId, msg)
···
1292
1293
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
1294
user := rp.oauth.GetUser(r)
1295
-
l := rp.logger.With("handler", "DeleteLabel")
1296
l = l.With("did", user.Did)
1297
l = l.With("handle", user.Handle)
1298
···
1302
return
1303
}
1304
1305
-
errorId := "label-operation"
1306
fail := func(msg string, err error) {
1307
l.Error(msg, "err", err)
1308
rp.pages.Notice(w, errorId, msg)
···
1361
rp.pages.HxRefresh(w)
1362
}
1363
1364
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
1365
user := rp.oauth.GetUser(r)
1366
l := rp.logger.With("handler", "AddCollaborator")
···
1790
return
1791
}
1792
1793
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
1794
if err != nil {
1795
log.Println("failed to fetch labels", err)
1796
rp.pages.Error503(w)
1797
return
1798
}
1799
1800
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
1801
-
LoggedInUser: user,
1802
-
RepoInfo: f.RepoInfo(user),
1803
-
Branches: result.Branches,
1804
-
Labels: labels,
1805
-
Tabs: settingsTabs,
1806
-
Tab: "general",
1807
})
1808
}
1809
···
966
rp.pages.HxRefresh(w)
967
}
968
969
+
func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) {
970
user := rp.oauth.GetUser(r)
971
l := rp.logger.With("handler", "AddLabel")
972
l = l.With("did", user.Did)
···
989
concreteType := r.FormValue("valueType")
990
valueFormat := r.FormValue("valueFormat")
991
enumValues := r.FormValue("enumValues")
992
+
scope := r.Form["scope"]
993
color := r.FormValue("color")
994
multiple := r.FormValue("multiple") == "true"
995
···
1000
}
1001
}
1002
1003
+
if concreteType == "" {
1004
+
concreteType = "null"
1005
+
}
1006
+
1007
format := db.ValueTypeFormatAny
1008
if valueFormat == "did" {
1009
format = db.ValueTypeFormatDid
···
1020
Rkey: tid.TID(),
1021
Name: name,
1022
ValueType: valueType,
1023
+
Scope: scope,
1024
Color: &color,
1025
Multiple: multiple,
1026
Created: time.Now(),
···
1076
Val: &repoRecord,
1077
},
1078
})
1079
+
if err != nil {
1080
+
fail("Failed to update labels for repo.", err)
1081
+
return
1082
+
}
1083
1084
tx, err := rp.db.BeginTx(r.Context(), nil)
1085
if err != nil {
···
1126
rp.pages.HxRefresh(w)
1127
}
1128
1129
+
func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) {
1130
user := rp.oauth.GetUser(r)
1131
l := rp.logger.With("handler", "DeleteLabel")
1132
l = l.With("did", user.Did)
···
1237
1238
func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) {
1239
user := rp.oauth.GetUser(r)
1240
+
l := rp.logger.With("handler", "SubscribeLabel")
1241
l = l.With("did", user.Did)
1242
l = l.With("handle", user.Handle)
1243
···
1247
return
1248
}
1249
1250
+
errorId := "default-label-operation"
1251
fail := func(msg string, err error) {
1252
l.Error(msg, "err", err)
1253
rp.pages.Notice(w, errorId, msg)
···
1300
1301
func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) {
1302
user := rp.oauth.GetUser(r)
1303
+
l := rp.logger.With("handler", "UnsubscribeLabel")
1304
l = l.With("did", user.Did)
1305
l = l.With("handle", user.Handle)
1306
···
1310
return
1311
}
1312
1313
+
errorId := "default-label-operation"
1314
fail := func(msg string, err error) {
1315
l.Error(msg, "err", err)
1316
rp.pages.Notice(w, errorId, msg)
···
1369
rp.pages.HxRefresh(w)
1370
}
1371
1372
+
func (rp *Repo) LabelPanel(w http.ResponseWriter, r *http.Request) {
1373
+
l := rp.logger.With("handler", "LabelPanel")
1374
+
1375
+
f, err := rp.repoResolver.Resolve(r)
1376
+
if err != nil {
1377
+
l.Error("failed to get repo and knot", "err", err)
1378
+
return
1379
+
}
1380
+
1381
+
subjectStr := r.FormValue("subject")
1382
+
subject, err := syntax.ParseATURI(subjectStr)
1383
+
if err != nil {
1384
+
l.Error("failed to get repo and knot", "err", err)
1385
+
return
1386
+
}
1387
+
1388
+
labelDefs, err := db.GetLabelDefinitions(
1389
+
rp.db,
1390
+
db.FilterIn("at_uri", f.Repo.Labels),
1391
+
db.FilterContains("scope", subject.Collection().String()),
1392
+
)
1393
+
if err != nil {
1394
+
log.Println("failed to fetch label defs", err)
1395
+
return
1396
+
}
1397
+
1398
+
defs := make(map[string]*db.LabelDefinition)
1399
+
for _, l := range labelDefs {
1400
+
defs[l.AtUri().String()] = &l
1401
+
}
1402
+
1403
+
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
1404
+
if err != nil {
1405
+
log.Println("failed to build label state", err)
1406
+
return
1407
+
}
1408
+
state := states[subject]
1409
+
1410
+
user := rp.oauth.GetUser(r)
1411
+
rp.pages.LabelPanel(w, pages.LabelPanelParams{
1412
+
LoggedInUser: user,
1413
+
RepoInfo: f.RepoInfo(user),
1414
+
Defs: defs,
1415
+
Subject: subject.String(),
1416
+
State: state,
1417
+
})
1418
+
}
1419
+
1420
+
func (rp *Repo) EditLabelPanel(w http.ResponseWriter, r *http.Request) {
1421
+
l := rp.logger.With("handler", "EditLabelPanel")
1422
+
1423
+
f, err := rp.repoResolver.Resolve(r)
1424
+
if err != nil {
1425
+
l.Error("failed to get repo and knot", "err", err)
1426
+
return
1427
+
}
1428
+
1429
+
subjectStr := r.FormValue("subject")
1430
+
subject, err := syntax.ParseATURI(subjectStr)
1431
+
if err != nil {
1432
+
l.Error("failed to get repo and knot", "err", err)
1433
+
return
1434
+
}
1435
+
1436
+
labelDefs, err := db.GetLabelDefinitions(
1437
+
rp.db,
1438
+
db.FilterIn("at_uri", f.Repo.Labels),
1439
+
db.FilterContains("scope", subject.Collection().String()),
1440
+
)
1441
+
if err != nil {
1442
+
log.Println("failed to fetch labels", err)
1443
+
return
1444
+
}
1445
+
1446
+
defs := make(map[string]*db.LabelDefinition)
1447
+
for _, l := range labelDefs {
1448
+
defs[l.AtUri().String()] = &l
1449
+
}
1450
+
1451
+
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
1452
+
if err != nil {
1453
+
log.Println("failed to build label state", err)
1454
+
return
1455
+
}
1456
+
state := states[subject]
1457
+
1458
+
user := rp.oauth.GetUser(r)
1459
+
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
1460
+
LoggedInUser: user,
1461
+
RepoInfo: f.RepoInfo(user),
1462
+
Defs: defs,
1463
+
Subject: subject.String(),
1464
+
State: state,
1465
+
})
1466
+
}
1467
+
1468
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
1469
user := rp.oauth.GetUser(r)
1470
l := rp.logger.With("handler", "AddCollaborator")
···
1894
return
1895
}
1896
1897
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs()))
1898
+
if err != nil {
1899
+
log.Println("failed to fetch labels", err)
1900
+
rp.pages.Error503(w)
1901
+
return
1902
+
}
1903
+
1904
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
1905
if err != nil {
1906
log.Println("failed to fetch labels", err)
1907
rp.pages.Error503(w)
1908
return
1909
}
1910
+
// remove default labels from the labels list, if present
1911
+
defaultLabelMap := make(map[string]bool)
1912
+
for _, dl := range defaultLabels {
1913
+
defaultLabelMap[dl.AtUri().String()] = true
1914
+
}
1915
+
n := 0
1916
+
for _, l := range labels {
1917
+
if !defaultLabelMap[l.AtUri().String()] {
1918
+
labels[n] = l
1919
+
n++
1920
+
}
1921
+
}
1922
+
labels = labels[:n]
1923
+
1924
+
subscribedLabels := make(map[string]struct{})
1925
+
for _, l := range f.Repo.Labels {
1926
+
subscribedLabels[l] = struct{}{}
1927
+
}
1928
1929
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
1930
+
LoggedInUser: user,
1931
+
RepoInfo: f.RepoInfo(user),
1932
+
Branches: result.Branches,
1933
+
Labels: labels,
1934
+
DefaultLabels: defaultLabels,
1935
+
SubscribedLabels: subscribedLabels,
1936
+
Tabs: settingsTabs,
1937
+
Tab: "general",
1938
})
1939
}
1940
+36
-12
appview/validator/label.go
+36
-12
appview/validator/label.go
···
18
// Color should be a valid hex color
19
colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
20
// You can only label issues and pulls presently
21
-
validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}
22
)
23
24
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
···
36
}
37
38
if !label.ValueType.IsConcreteType() {
39
-
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
40
}
41
42
-
if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
43
return fmt.Errorf("null type cannot be used in conjunction with enum type")
44
}
45
46
// validate scope (nsid format)
47
-
if label.Scope == "" {
48
return fmt.Errorf("scope is required")
49
}
50
-
if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {
51
-
return fmt.Errorf("failed to parse scope: %w", err)
52
-
}
53
-
if !slices.Contains(validScopes, label.Scope) {
54
-
return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)
55
}
56
57
// validate color if provided
···
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
···
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
}
···
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
}
···
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
}
···
18
// Color should be a valid hex color
19
colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
20
// You can only label issues and pulls presently
21
+
validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
22
)
23
24
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
···
36
}
37
38
if !label.ValueType.IsConcreteType() {
39
+
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type)
40
}
41
42
+
// null type checks: cannot be enums, multiple or explicit format
43
+
if label.ValueType.IsNull() && label.ValueType.IsEnum() {
44
return fmt.Errorf("null type cannot be used in conjunction with enum type")
45
}
46
+
if label.ValueType.IsNull() && label.Multiple {
47
+
return fmt.Errorf("null type labels cannot be multiple")
48
+
}
49
+
if label.ValueType.IsNull() && !label.ValueType.IsAnyFormat() {
50
+
return fmt.Errorf("format cannot be used in conjunction with null type")
51
+
}
52
+
53
+
// format checks: cannot be used with enum, or integers
54
+
if !label.ValueType.IsAnyFormat() && label.ValueType.IsEnum() {
55
+
return fmt.Errorf("enum types cannot be used in conjunction with format specification")
56
+
}
57
+
58
+
if !label.ValueType.IsAnyFormat() && !label.ValueType.IsString() {
59
+
return fmt.Errorf("format specifications are only permitted on string types")
60
+
}
61
62
// validate scope (nsid format)
63
+
if label.Scope == nil {
64
return fmt.Errorf("scope is required")
65
}
66
+
for _, s := range label.Scope {
67
+
if _, err := syntax.ParseNSID(s); err != nil {
68
+
return fmt.Errorf("failed to parse scope: %w", err)
69
+
}
70
+
if !slices.Contains(validScopes, s) {
71
+
return fmt.Errorf("invalid scope: scope must be present in %q", validScopes)
72
+
}
73
}
74
75
// validate color if provided
···
134
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
135
valueType := labelDef.ValueType
136
137
+
// this is permitted, it "unsets" a label
138
+
if labelOp.OperandValue == "" {
139
+
labelOp.Operation = db.LabelOperationDel
140
+
return nil
141
+
}
142
+
143
switch valueType.Type {
144
case db.ConcreteTypeNull:
145
// For null type, value should be empty
···
149
150
case db.ConcreteTypeString:
151
// For string type, validate enum constraints if present
152
+
if valueType.IsEnum() {
153
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
154
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
155
}
···
177
return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
178
}
179
180
+
if valueType.IsEnum() {
181
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
182
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
183
}
···
189
}
190
191
// validate enum constraints if present (though uncommon for booleans)
192
+
if valueType.IsEnum() {
193
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
194
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
195
}