Skip to content

Commit 5a5b743

Browse files
authored
Merge pull request #362 from hjotha/submit/add-expression-to-list
feat: allow expressions in add-to-list statements
2 parents 0c374cf + fa1c909 commit 5a5b743

20 files changed

Lines changed: 355 additions & 32 deletions

.claude/skills/mendix/write-microflows.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,18 @@ commit $Product with events refresh;
420420

421421
**Best Practice**: Use `with events` when you want before/after commit event handlers to execute. Use `refresh` when the committed object is displayed in the client and you want the UI to update immediately.
422422

423+
## List Operations
424+
425+
```mdl
426+
-- Existing variable form
427+
add $Item to $Items;
428+
429+
-- Expression-valued add, useful when round-tripping Studio Pro list-add values
430+
add head($SourceItems) to $Items;
431+
```
432+
433+
Use expression-valued `add` only when the expression returns an object compatible with the target list element type.
434+
423435
## Database Operations
424436

425437
### RETRIEVE Statement

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ authentication basic, session
224224
| Rollback | `rollback $entity [refresh];` | Reverts uncommitted changes |
225225
| Retrieve (DB) | `retrieve $Var from Module.Entity [where condition];` | Database XPath retrieve |
226226
| Retrieve (Assoc) | `retrieve $list from $Parent/Module.AssocName;` | Retrieve by association |
227+
| Add to list | `add expression to $list;` | Also accepts existing `add $item to $list;` form |
227228
| Call microflow | `$Result = call microflow Module.Name (Param = $value);` | |
228229
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
229230
| Call JS action | `$Result = call javascript action Module.Name (Param = $value);` | JavaScript action (nanoflow/microflow) |
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Proposal: Microflow ADD Expression To List
2+
3+
Status: Draft
4+
5+
## Summary
6+
7+
Allow `add` microflow statements to use any expression as the value being added to a list:
8+
9+
```mdl
10+
add if $UseFirst then $FirstItem else $SecondItem to $TargetList;
11+
```
12+
13+
The existing variable-only form remains valid:
14+
15+
```mdl
16+
add $Item to $TargetList;
17+
```
18+
19+
## Motivation
20+
21+
Studio Pro stores the value of a list-add action as an expression string. Existing models can therefore contain a list-add value that is not a bare variable. MDL previously parsed only `add $Item to $List`, so describe/exec round-trips could not preserve expression-valued list additions.
22+
23+
## Semantics
24+
25+
The parser stores the add value as an expression. For compatibility, a bare variable expression also populates the legacy `Item` field in the AST. The builder writes the expression source to the Mendix `ChangeListAction.Value` field and falls back to the legacy item variable only when no expression is present.
26+
27+
## Tests And Examples
28+
29+
`mdl-examples/doctype-tests/add_expression_to_list.mdl` demonstrates adding an object-valued conditional expression to another list. Go regression tests cover parser behavior and builder output for both expression and simple-variable forms.
30+
31+
## Open Questions
32+
33+
- Should validation infer the list element type and reject expressions that cannot produce an object compatible with the target list?

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ BSON schema Registry ◄──── multi-version Support
5353
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain ||
5454
| [Microflow ENUM SPLIT Statement](PROPOSAL_microflow_enum_split_statement.md) | Implemented | Enumeration decision splits via `case $Var when Value then … end case;` ||
5555
| [Microflow CHANGE Refresh Modifier](PROPOSAL_microflow_change_refresh_modifier.md) | Draft | Preserve `RefreshInClient` on change-object actions ||
56+
| [Microflow ADD Expression To List](PROPOSAL_microflow_add_expression_to_list.md) | Draft | Preserve expression-valued list-add actions in microflow round-trips ||
5657
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case ||
5758

5859
### Testing & Evaluation
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
create module AddListExample;
2+
3+
create persistent entity AddListExample.Item (
4+
Name: string
5+
);
6+
/
7+
8+
create microflow AddListExample.AddChosenItem (
9+
$UseFirst: Boolean
10+
)
11+
returns list of AddListExample.Item as $Items
12+
begin
13+
$First = create AddListExample.Item (Name = 'first');
14+
$Second = create AddListExample.Item (Name = 'second');
15+
$Items = create list of AddListExample.Item;
16+
add if $UseFirst then $First else $Second to $Items;
17+
return $Items;
18+
end;
19+
/

mdl/ast/ast_microflow.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,10 @@ type CreateListStmt struct {
583583

584584
func (s *CreateListStmt) isMicroflowStatement() {}
585585

586-
// AddToListStmt represents: ADD $Item TO $List
586+
// AddToListStmt represents: ADD expr TO $List
587587
type AddToListStmt struct {
588-
Item string // Item variable to add
588+
Item string // Item variable to add, kept for simple $Var compatibility
589+
Value Expression // Item expression to add
589590
List string // Target list variable
590591
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
591592
}

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,11 +1080,15 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID {
10801080

10811081
// addAddToListAction creates an ADD TO list statement.
10821082
func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID {
1083+
value := fb.exprToString(s.Value)
1084+
if value == "" && s.Item != "" {
1085+
value = "$" + s.Item
1086+
}
10831087
action := &microflows.ChangeListAction{
10841088
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
10851089
Type: microflows.ChangeListTypeAdd,
10861090
ChangeVariable: s.List,
1087-
Value: "$" + s.Item,
1091+
Value: value,
10881092
}
10891093

10901094
activity := &microflows.ActionActivity{
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
"github.com/mendixlabs/mxcli/sdk/microflows"
10+
)
11+
12+
func TestAddToListBuilderUsesExpressionValue(t *testing.T) {
13+
fb := &flowBuilder{}
14+
15+
fb.addAddToListAction(&ast.AddToListStmt{
16+
Value: &ast.AttributePathExpr{
17+
Variable: "Order",
18+
Path: []string{"Number"},
19+
},
20+
List: "Numbers",
21+
})
22+
23+
action := lastChangeListAction(t, fb)
24+
if action.Value != "$Order/Number" {
25+
t.Fatalf("Value = %q, want $Order/Number", action.Value)
26+
}
27+
}
28+
29+
func TestAddToListBuilderKeepsSimpleVariableFallback(t *testing.T) {
30+
fb := &flowBuilder{}
31+
32+
fb.addAddToListAction(&ast.AddToListStmt{
33+
Item: "Order",
34+
List: "Orders",
35+
})
36+
37+
action := lastChangeListAction(t, fb)
38+
if action.Value != "$Order" {
39+
t.Fatalf("Value = %q, want $Order", action.Value)
40+
}
41+
}
42+
43+
func TestCollectObjectInputVariablesSeesAddExpressionValue(t *testing.T) {
44+
inputs := collectObjectInputVariables([]ast.MicroflowStatement{
45+
&ast.AddToListStmt{
46+
Value: &ast.FunctionCallExpr{
47+
Name: "head",
48+
Arguments: []ast.Expression{
49+
&ast.VariableExpr{Name: "SourceItems"},
50+
},
51+
},
52+
List: "Items",
53+
},
54+
})
55+
56+
if !inputs["SourceItems"] {
57+
t.Fatalf("SourceItems was not collected from add expression: %#v", inputs)
58+
}
59+
}
60+
61+
func TestErrorHandlerStatementVarRefsSeesAddExpressionValue(t *testing.T) {
62+
stmt := &ast.AddToListStmt{
63+
Value: &ast.FunctionCallExpr{
64+
Name: "head",
65+
Arguments: []ast.Expression{
66+
&ast.VariableExpr{Name: "SourceItems"},
67+
},
68+
},
69+
List: "Items",
70+
}
71+
72+
refs := errorHandlerStatementVarRefs(stmt)
73+
74+
seenSource := false
75+
seenList := false
76+
for _, r := range refs {
77+
if r == "SourceItems" {
78+
seenSource = true
79+
}
80+
if r == "Items" {
81+
seenList = true
82+
}
83+
}
84+
if !seenSource {
85+
t.Errorf("expected $SourceItems to be tracked from add expression: %v", refs)
86+
}
87+
if !seenList {
88+
t.Errorf("expected $Items (list) to be tracked: %v", refs)
89+
}
90+
}
91+
92+
func lastChangeListAction(t *testing.T, fb *flowBuilder) *microflows.ChangeListAction {
93+
t.Helper()
94+
95+
if len(fb.objects) == 0 {
96+
t.Fatal("Expected builder to create an action activity")
97+
}
98+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
99+
if !ok {
100+
t.Fatalf("Last object = %T, want ActionActivity", fb.objects[len(fb.objects)-1])
101+
}
102+
action, ok := activity.Action.(*microflows.ChangeListAction)
103+
if !ok {
104+
t.Fatalf("Action = %T, want ChangeListAction", activity.Action)
105+
}
106+
return action
107+
}

mdl/executor/cmd_microflows_builder_flows.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,12 @@ func errorHandlerStatementVarRefs(stmt ast.MicroflowStatement) []string {
607607
refs = append(refs, exprVarRefs(arg)...)
608608
}
609609
case *ast.AddToListStmt:
610-
refs = append(refs, s.Item, s.List)
610+
if s.Value != nil {
611+
refs = append(refs, exprVarRefs(s.Value)...)
612+
} else if s.Item != "" {
613+
refs = append(refs, s.Item)
614+
}
615+
refs = append(refs, s.List)
611616
case *ast.RemoveFromListStmt:
612617
refs = append(refs, s.Item, s.List)
613618
}

mdl/executor/cmd_microflows_builder_graph.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,11 @@ func collectObjectInputVariables(stmts []ast.MicroflowStatement) map[string]bool
330330
case *ast.AggregateListStmt:
331331
walkExpr(s.Expression)
332332
case *ast.AddToListStmt:
333-
if s.Item != "" {
333+
if s.Value != nil {
334+
for _, ref := range exprVarRefs(s.Value) {
335+
inputs[ref] = true
336+
}
337+
} else if s.Item != "" {
334338
inputs[s.Item] = true
335339
}
336340
case *ast.CallMicroflowStmt:

0 commit comments

Comments
 (0)