Skip to content

Commit 22eb6c4

Browse files
committed
refactor: consolidate action condition evaluation
1 parent 2d22577 commit 22eb6c4

2 files changed

Lines changed: 76 additions & 75 deletions

File tree

internal/services/action_service.go

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,45 +1241,11 @@ func (as *ActionService) executeSetFieldCustom(ctx *models.ExecutionContext, ste
12411241
return err
12421242
}
12431243

1244-
// Validate options and sanitize substituted user content before persisting.
1245-
fieldKey := strconv.Itoa(config.CustomFieldID)
1246-
cfv := map[string]any{fieldKey: value}
1247-
fieldTypes, err := validation.CustomFieldTypes(as.db, cfv)
1244+
normalized, err := as.normalizeSetFieldCustomValue(config.CustomFieldID, value)
12481245
if err != nil {
1249-
return fmt.Errorf("resolve custom field type: %w", err)
1250-
}
1251-
fieldType, ok := fieldTypes[fieldKey]
1252-
if !ok {
1253-
return fmt.Errorf("set_field: custom field %d does not exist", config.CustomFieldID)
1254-
}
1255-
switch fieldType {
1256-
case "select":
1257-
// An empty substitution clears the field rather than failing
1258-
// option-id validation.
1259-
if strings.TrimSpace(value) == "" {
1260-
cfv[fieldKey] = nil
1261-
}
1262-
case "multiselect":
1263-
// Multiselect values arrive as the substituted string form of a
1264-
// JSON array ("[1,2]") or a CSV of option ids — decode before
1265-
// validation so each element is checked against the option set.
1266-
cfv[fieldKey] = parseActionMultiselectValue(value)
1267-
case models.CustomFieldTypeBoolean, models.CustomFieldTypeCheckbox:
1268-
switch strings.ToLower(strings.TrimSpace(value)) {
1269-
case "":
1270-
cfv[fieldKey] = nil
1271-
case "true":
1272-
cfv[fieldKey] = true
1273-
case "false":
1274-
cfv[fieldKey] = false
1275-
default:
1276-
return fmt.Errorf("set_field: custom field %d requires true or false", config.CustomFieldID)
1277-
}
1278-
}
1279-
if err := validation.ValidateAndNormalizeCustomFieldValues(as.db, cfv); err != nil {
1280-
return fmt.Errorf("set_field: custom field %d: %w", config.CustomFieldID, err)
1246+
return err
12811247
}
1282-
newValue := cfv[fieldKey]
1248+
newValue := normalized.value
12831249

12841250
oldValue, err := as.itemRepo.GetItemCustomFieldValue(itemID, config.CustomFieldID)
12851251
if err != nil {
@@ -1299,15 +1265,15 @@ func (as *ActionService) executeSetFieldCustom(ctx *models.ExecutionContext, ste
12991265
for key, existingValue := range item.CustomFieldValues {
13001266
customFieldValues[key] = existingValue
13011267
}
1302-
customFieldValues[fieldKey] = newValue
1268+
customFieldValues[normalized.key] = newValue
13031269
result, err := as.updateItemFromAction(ctx, map[string]any{
13041270
"custom_field_values": customFieldValues,
13051271
})
13061272
if err != nil {
13071273
return err
13081274
}
13091275
if result.Item.CustomFieldValues != nil {
1310-
newValue = result.Item.CustomFieldValues[fieldKey]
1276+
newValue = result.Item.CustomFieldValues[normalized.key]
13111277
}
13121278

13131279
key := "custom_field_" + strconv.Itoa(config.CustomFieldID)
@@ -1321,6 +1287,47 @@ func (as *ActionService) executeSetFieldCustom(ctx *models.ExecutionContext, ste
13211287
return nil
13221288
}
13231289

1290+
type normalizedActionCustomField struct {
1291+
key string
1292+
value any
1293+
}
1294+
1295+
func (as *ActionService) normalizeSetFieldCustomValue(customFieldID int, value string) (normalizedActionCustomField, error) {
1296+
fieldKey := strconv.Itoa(customFieldID)
1297+
values := map[string]any{fieldKey: value}
1298+
fieldTypes, err := validation.CustomFieldTypes(as.db, values)
1299+
if err != nil {
1300+
return normalizedActionCustomField{}, fmt.Errorf("resolve custom field type: %w", err)
1301+
}
1302+
fieldType, ok := fieldTypes[fieldKey]
1303+
if !ok {
1304+
return normalizedActionCustomField{}, fmt.Errorf("set_field: custom field %d does not exist", customFieldID)
1305+
}
1306+
switch fieldType {
1307+
case "select":
1308+
if strings.TrimSpace(value) == "" {
1309+
values[fieldKey] = nil
1310+
}
1311+
case "multiselect":
1312+
values[fieldKey] = parseActionMultiselectValue(value)
1313+
case models.CustomFieldTypeBoolean, models.CustomFieldTypeCheckbox:
1314+
switch strings.ToLower(strings.TrimSpace(value)) {
1315+
case "":
1316+
values[fieldKey] = nil
1317+
case "true":
1318+
values[fieldKey] = true
1319+
case "false":
1320+
values[fieldKey] = false
1321+
default:
1322+
return normalizedActionCustomField{}, fmt.Errorf("set_field: custom field %d requires true or false", customFieldID)
1323+
}
1324+
}
1325+
if err := validation.ValidateAndNormalizeCustomFieldValues(as.db, values); err != nil {
1326+
return normalizedActionCustomField{}, fmt.Errorf("set_field: custom field %d: %w", customFieldID, err)
1327+
}
1328+
return normalizedActionCustomField{key: fieldKey, value: values[fieldKey]}, nil
1329+
}
1330+
13241331
// parseActionMultiselectValue decodes a substituted multiselect set_field
13251332
// value: a JSON array ("[1,2]") or a CSV of option ids ("1, 2"). An empty
13261333
// string means "clear". Elements stay untyped — option-id coercion and
@@ -1815,20 +1822,10 @@ func (as *ActionService) executeCondition(node *models.ActionNode, ctx *models.E
18151822
// evaluateCondition evaluates a condition
18161823
func (as *ActionService) evaluateCondition(value any, operator, compareValue string) bool {
18171824
strValue := fmt.Sprintf("%v", value)
1818-
1825+
if result, handled := evaluateStringActionCondition(strValue, operator, compareValue); handled {
1826+
return result
1827+
}
18191828
switch operator {
1820-
case "eq", "==", "equals":
1821-
return strValue == compareValue
1822-
case "ne", "!=", "not_equals":
1823-
return strValue != compareValue
1824-
case "contains":
1825-
return strings.Contains(strValue, compareValue)
1826-
case "not_contains":
1827-
return !strings.Contains(strValue, compareValue)
1828-
case "starts_with":
1829-
return strings.HasPrefix(strValue, compareValue)
1830-
case "ends_with":
1831-
return strings.HasSuffix(strValue, compareValue)
18321829
case "gt", ">":
18331830
return compareNumericOrString(strValue, compareValue, func(a, b float64) bool { return a > b }, func(a, b string) bool { return a > b })
18341831
case "lt", "<":
@@ -1837,12 +1834,31 @@ func (as *ActionService) evaluateCondition(value any, operator, compareValue str
18371834
return compareNumericOrString(strValue, compareValue, func(a, b float64) bool { return a >= b }, func(a, b string) bool { return a >= b })
18381835
case "lte", "<=":
18391836
return compareNumericOrString(strValue, compareValue, func(a, b float64) bool { return a <= b }, func(a, b string) bool { return a <= b })
1837+
default:
1838+
return false
1839+
}
1840+
}
1841+
1842+
func evaluateStringActionCondition(value, operator, compareValue string) (matched, handled bool) {
1843+
switch operator {
1844+
case "eq", "==", "equals":
1845+
return value == compareValue, true
1846+
case "ne", "!=", "not_equals":
1847+
return value != compareValue, true
1848+
case "contains":
1849+
return strings.Contains(value, compareValue), true
1850+
case "not_contains":
1851+
return !strings.Contains(value, compareValue), true
1852+
case "starts_with":
1853+
return strings.HasPrefix(value, compareValue), true
1854+
case "ends_with":
1855+
return strings.HasSuffix(value, compareValue), true
18401856
case "is_empty":
1841-
return strValue == "" || strValue == "null" || strValue == "<nil>"
1857+
return value == "" || value == "null" || value == "<nil>", true
18421858
case "is_not_empty":
1843-
return strValue != "" && strValue != "null" && strValue != "<nil>"
1859+
return value != "" && value != "null" && value != "<nil>", true
18441860
default:
1845-
return false
1861+
return false, false
18461862
}
18471863
}
18481864

internal/services/asset_action_service.go

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ func (as *AssetActionService) executeCondition(node *models.AssetActionNode, ctx
870870
fieldValue = ctx.Variables["new_"+config.FieldName]
871871
}
872872

873-
result := evaluateCondition(fieldValue, config.Operator, config.Value)
873+
result := evaluateAssetActionCondition(fieldValue, config.Operator, config.Value)
874874

875875
stepResult.Output = map[string]any{
876876
"condition_result": result,
@@ -974,23 +974,12 @@ func (as *AssetActionService) canExecuteNode(nodeID int, edges []models.AssetAct
974974
return actionutil.CanExecuteNodeTyped(nodeID, edges, executedNodes, ctx.StepResults)
975975
}
976976

977-
// evaluateCondition evaluates a condition (reused from workspace action service)
978-
func evaluateCondition(value any, operator, compareValue string) bool {
977+
func evaluateAssetActionCondition(value any, operator, compareValue string) bool {
979978
strValue := fmt.Sprintf("%v", value)
980-
979+
if result, handled := evaluateStringActionCondition(strValue, operator, compareValue); handled {
980+
return result
981+
}
981982
switch operator {
982-
case "eq", "==", "equals":
983-
return strValue == compareValue
984-
case "ne", "!=", "not_equals":
985-
return strValue != compareValue
986-
case "contains":
987-
return strings.Contains(strValue, compareValue)
988-
case "not_contains":
989-
return !strings.Contains(strValue, compareValue)
990-
case "starts_with":
991-
return strings.HasPrefix(strValue, compareValue)
992-
case "ends_with":
993-
return strings.HasSuffix(strValue, compareValue)
994983
case "gt", ">":
995984
if numVal, err := strconv.ParseFloat(strValue, 64); err == nil {
996985
if numCompare, err := strconv.ParseFloat(compareValue, 64); err == nil {
@@ -1005,10 +994,6 @@ func evaluateCondition(value any, operator, compareValue string) bool {
1005994
}
1006995
}
1007996
return strValue < compareValue
1008-
case "is_empty":
1009-
return strValue == "" || strValue == "null" || strValue == "<nil>"
1010-
case "is_not_empty":
1011-
return strValue != "" && strValue != "null" && strValue != "<nil>"
1012997
default:
1013998
return false
1014999
}

0 commit comments

Comments
 (0)