Skip to content

Commit a8dee83

Browse files
committed
fix: preserve Java action microflow placeholder parameters
Symptom: Java actions with microflow-typed parameters roundtripped placeholder arguments as basic empty expressions, which Studio Pro reported as stale Java action arguments. Root cause: the SDK/parser/writer did not model Microflows$MicroflowParameterValue, and the builder treated all ellipsis placeholders as BasicCodeActionParameterValue. Fix: add MicroflowParameterValue support end-to-end and emit it for Java action parameters whose definition is MicroflowType, keeping ellipsis as an empty microflow reference. Tests: added builder and parser regressions and ran make test.
1 parent 30287e5 commit a8dee83

10 files changed

Lines changed: 167 additions & 29 deletions

mdl/executor/cmd_microflows_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type flowBuilder struct {
5353
microflowsCacheLoaded bool
5454
nanoflowsCache []*microflows.Nanoflow
5555
nanoflowsCacheLoaded bool
56-
manualLoopBackTarget model.ID
56+
manualLoopBackTarget model.ID
5757
}
5858

5959
// addError records a validation error during flow building.

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,13 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
244244

245245
// Build a map of parameter name -> param type for the Java action
246246
entityTypeParams := make(map[string]bool)
247+
microflowTypeParams := make(map[string]bool)
247248
if jaDef != nil {
248249
for _, p := range jaDef.Parameters {
249250
if _, ok := p.ParameterType.(*javaactions.EntityTypeParameterType); ok {
250251
entityTypeParams[p.Name] = true
252+
} else if _, ok := p.ParameterType.(*javaactions.MicroflowType); ok {
253+
microflowTypeParams[p.Name] = true
251254
}
252255
}
253256
}
@@ -277,16 +280,30 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
277280
Entity: entityName,
278281
}
279282
} else if isPlaceholderExpression(arg.Value) {
280-
value = &microflows.BasicCodeActionParameterValue{
281-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
282-
Argument: "",
283+
if microflowTypeParams[arg.Name] {
284+
value = &microflows.MicroflowParameterValue{
285+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
286+
Microflow: "",
287+
}
288+
} else {
289+
value = &microflows.BasicCodeActionParameterValue{
290+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
291+
Argument: "",
292+
}
283293
}
284294
} else {
285295
// Regular parameter: expression-based value
286296
valueExpr := fb.exprToString(arg.Value)
287-
value = &microflows.BasicCodeActionParameterValue{
288-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
289-
Argument: valueExpr,
297+
if microflowTypeParams[arg.Name] {
298+
value = &microflows.MicroflowParameterValue{
299+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
300+
Microflow: strings.Trim(valueExpr, "'"),
301+
}
302+
} else {
303+
value = &microflows.BasicCodeActionParameterValue{
304+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
305+
Argument: valueExpr,
306+
}
290307
}
291308
}
292309

mdl/executor/cmd_microflows_builder_java_action_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"testing"
77

88
"github.com/mendixlabs/mxcli/mdl/ast"
9+
"github.com/mendixlabs/mxcli/mdl/backend/mock"
10+
"github.com/mendixlabs/mxcli/model"
11+
"github.com/mendixlabs/mxcli/sdk/javaactions"
912
"github.com/mendixlabs/mxcli/sdk/microflows"
1013
)
1114

@@ -57,3 +60,57 @@ func TestBuildJavaAction_PlaceholderArgumentPreservesEmptyBasicValue(t *testing.
5760
t.Fatalf("boolean argument = %q, want true", value.Argument)
5861
}
5962
}
63+
64+
func TestBuildJavaAction_PlaceholderMicroflowArgumentUsesMicroflowParameterValue(t *testing.T) {
65+
fb := &flowBuilder{
66+
posX: 100,
67+
posY: 100,
68+
spacing: HorizontalSpacing,
69+
backend: &mock.MockBackend{
70+
ReadJavaActionByNameFunc: func(qualifiedName string) (*javaactions.JavaAction, error) {
71+
if qualifiedName != "SampleModule.StartAsync" {
72+
t.Fatalf("java action lookup = %q", qualifiedName)
73+
}
74+
return &javaactions.JavaAction{
75+
Parameters: []*javaactions.JavaActionParameter{
76+
{
77+
Name: "Callback",
78+
ParameterType: &javaactions.MicroflowType{
79+
BaseElement: model.BaseElement{ID: "param-type"},
80+
},
81+
},
82+
},
83+
}, nil
84+
},
85+
},
86+
}
87+
stmt := &ast.CallJavaActionStmt{
88+
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "StartAsync"},
89+
Arguments: []ast.CallArgument{
90+
{Name: "Callback", Value: &ast.SourceExpr{Source: "..."}},
91+
},
92+
}
93+
94+
id := fb.addCallJavaActionAction(stmt)
95+
var activity *microflows.ActionActivity
96+
for _, obj := range fb.objects {
97+
if obj.GetID() == id {
98+
activity, _ = obj.(*microflows.ActionActivity)
99+
break
100+
}
101+
}
102+
if activity == nil {
103+
t.Fatal("expected Java action activity")
104+
}
105+
action, ok := activity.Action.(*microflows.JavaActionCallAction)
106+
if !ok {
107+
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
108+
}
109+
value, ok := action.ParameterMappings[0].Value.(*microflows.MicroflowParameterValue)
110+
if !ok {
111+
t.Fatalf("mapping value = %T, want *MicroflowParameterValue", action.ParameterMappings[0].Value)
112+
}
113+
if value.Microflow != "" {
114+
t.Fatalf("placeholder microflow = %q, want empty string", value.Microflow)
115+
}
116+
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -351,30 +351,30 @@ func formatAction(
351351

352352
stmt := fmt.Sprintf("retrieve $%s from %s", outputVar, entityName)
353353

354-
if dbSource.XPathConstraint != "" {
355-
constraint := strings.TrimSpace(dbSource.XPathConstraint)
356-
// XPath may contain multiple predicates like [a][b] or [a]\n[b].
357-
// Split them and join with MDL 'and' so the parser sees
358-
// separate xpathConstraint nodes.
359-
if strings.HasPrefix(constraint, "[") && strings.HasSuffix(constraint, "]") {
360-
// Split on "][" boundary (possibly separated by \n literals),
361-
// then re-wrap each predicate.
362-
inner := constraint[1 : len(constraint)-1]
363-
// Normalise real newlines between predicates: ]\n[ → ][
364-
inner = strings.ReplaceAll(inner, "]\n[", "][")
365-
parts := strings.Split(inner, "][")
366-
if len(parts) > 1 {
367-
var wrapped []string
368-
for _, p := range parts {
369-
wrapped = append(wrapped, "["+strings.TrimSpace(p)+"]")
354+
if dbSource.XPathConstraint != "" {
355+
constraint := strings.TrimSpace(dbSource.XPathConstraint)
356+
// XPath may contain multiple predicates like [a][b] or [a]\n[b].
357+
// Split them and join with MDL 'and' so the parser sees
358+
// separate xpathConstraint nodes.
359+
if strings.HasPrefix(constraint, "[") && strings.HasSuffix(constraint, "]") {
360+
// Split on "][" boundary (possibly separated by \n literals),
361+
// then re-wrap each predicate.
362+
inner := constraint[1 : len(constraint)-1]
363+
// Normalise real newlines between predicates: ]\n[ → ][
364+
inner = strings.ReplaceAll(inner, "]\n[", "][")
365+
parts := strings.Split(inner, "][")
366+
if len(parts) > 1 {
367+
var wrapped []string
368+
for _, p := range parts {
369+
wrapped = append(wrapped, "["+strings.TrimSpace(p)+"]")
370+
}
371+
constraint = strings.Join(wrapped, "\n ")
372+
} else {
373+
constraint = parts[0]
370374
}
371-
constraint = strings.Join(wrapped, "\n ")
372-
} else {
373-
constraint = parts[0]
374375
}
376+
stmt += fmt.Sprintf("\n where %s", constraint)
375377
}
376-
stmt += fmt.Sprintf("\n where %s", constraint)
377-
}
378378

379379
// Output SORT BY clause if present
380380
if len(dbSource.Sorting) > 0 {
@@ -541,6 +541,10 @@ func formatAction(
541541
valueStr = v.Expression
542542
case *microflows.BasicCodeActionParameterValue:
543543
valueStr = v.Argument
544+
case *microflows.MicroflowParameterValue:
545+
if v.Microflow != "" {
546+
valueStr = mdlQuote(v.Microflow)
547+
}
544548
case *microflows.EntityTypeCodeActionParameterValue:
545549
if v.Entity != "" {
546550
valueStr = mdlQuote(v.Entity)

sdk/microflows/microflows_actions.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,15 @@ type BasicCodeActionParameterValue struct {
616616

617617
func (BasicCodeActionParameterValue) isCodeActionParameterValue() {}
618618

619+
// MicroflowParameterValue is a microflow reference passed to a Java action
620+
// microflow parameter. An empty Microflow keeps Studio Pro's placeholder value.
621+
type MicroflowParameterValue struct {
622+
model.BaseElement
623+
Microflow string `json:"microflow,omitempty"` // BY_NAME_REFERENCE: qualified microflow name
624+
}
625+
626+
func (MicroflowParameterValue) isCodeActionParameterValue() {}
627+
619628
// EntityTypeCodeActionParameterValue is an entity type passed at a call site for a type parameter.
620629
type EntityTypeCodeActionParameterValue struct {
621630
model.BaseElement

sdk/mpr/parser_javaactions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ func parseCodeActionParameterType(raw map[string]any) javaactions.CodeActionPara
402402
}
403403
et.Enumeration = extractString(raw["Enumeration"])
404404
return et
405-
case "CodeActions$MicroflowType":
405+
case "CodeActions$MicroflowType", "JavaActions$MicroflowJavaActionParameterType":
406406
return &javaactions.MicroflowType{
407407
BaseElement: model.BaseElement{ID: model.ID(extractBsonID(raw["$ID"]))},
408408
}
@@ -461,6 +461,10 @@ func parseInnerParameterType(raw map[string]any) javaactions.CodeActionParameter
461461
return &javaactions.DateTimeType{
462462
BaseElement: model.BaseElement{ID: model.ID(extractBsonID(raw["$ID"]))},
463463
}
464+
case "CodeActions$MicroflowType", "JavaActions$MicroflowJavaActionParameterType":
465+
return &javaactions.MicroflowType{
466+
BaseElement: model.BaseElement{ID: model.ID(extractBsonID(raw["$ID"]))},
467+
}
464468
case "CodeActions$ConcreteEntityType", "CodeActions$EntityType":
465469
et := &javaactions.EntityType{
466470
BaseElement: model.BaseElement{ID: model.ID(extractBsonID(raw["$ID"]))},

sdk/mpr/parser_javaactions_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package mpr
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/sdk/javaactions"
9+
)
10+
11+
func TestParseCodeActionParameterType_JavaActionMicroflowParameter(t *testing.T) {
12+
value := parseCodeActionParameterType(map[string]any{
13+
"$ID": "type-1",
14+
"$Type": "JavaActions$MicroflowJavaActionParameterType",
15+
})
16+
17+
if _, ok := value.(*javaactions.MicroflowType); !ok {
18+
t.Fatalf("value = %T, want *MicroflowType", value)
19+
}
20+
}

sdk/mpr/parser_microflow_actions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ func parseCodeActionParameterValue(raw map[string]any) microflows.CodeActionPara
175175
value.ID = model.ID(extractBsonID(raw["$ID"]))
176176
value.Argument = extractString(raw["Argument"])
177177
return value
178+
case "Microflows$MicroflowParameterValue":
179+
value := &microflows.MicroflowParameterValue{}
180+
value.ID = model.ID(extractBsonID(raw["$ID"]))
181+
value.Microflow = extractString(raw["Microflow"])
182+
return value
178183
case "Microflows$EntityTypeCodeActionParameterValue":
179184
value := &microflows.EntityTypeCodeActionParameterValue{}
180185
value.ID = model.ID(extractBsonID(raw["$ID"]))

sdk/mpr/parser_microflow_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,19 @@ func TestParseCommitAction_ErrorHandlingTypeDefaultsToRollback(t *testing.T) {
8181
t.Errorf("expected default Rollback, got %q", action.ErrorHandlingType)
8282
}
8383
}
84+
85+
func TestParseCodeActionParameterValue_MicroflowParameterValue(t *testing.T) {
86+
value := parseCodeActionParameterValue(map[string]any{
87+
"$ID": "value-1",
88+
"$Type": "Microflows$MicroflowParameterValue",
89+
"Microflow": "SyntheticModule.Callback",
90+
})
91+
92+
got, ok := value.(*microflows.MicroflowParameterValue)
93+
if !ok {
94+
t.Fatalf("value = %T, want *MicroflowParameterValue", value)
95+
}
96+
if got.Microflow != "SyntheticModule.Callback" {
97+
t.Fatalf("microflow = %q", got.Microflow)
98+
}
99+
}

sdk/mpr/writer_microflow_actions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,12 @@ func serializeCodeActionParameterValue(v microflows.CodeActionParameterValue) bs
11811181
{Key: "$Type", Value: "Microflows$BasicCodeActionParameterValue"},
11821182
{Key: "Argument", Value: value.Argument},
11831183
}
1184+
case *microflows.MicroflowParameterValue:
1185+
return bson.D{
1186+
{Key: "$ID", Value: idToBsonBinary(string(value.ID))},
1187+
{Key: "$Type", Value: "Microflows$MicroflowParameterValue"},
1188+
{Key: "Microflow", Value: value.Microflow},
1189+
}
11841190
case *microflows.EntityTypeCodeActionParameterValue:
11851191
return bson.D{
11861192
{Key: "$ID", Value: idToBsonBinary(string(value.ID))},

0 commit comments

Comments
 (0)