Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions mdl/executor/bugfix_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,53 @@ func TestCallMicroflowResultType_ResolvesSubsequentChangeMember(t *testing.T) {
}
}

func TestListFindAttributeEqualsExpressionUsesAttributeOperation(t *testing.T) {
fb := &flowBuilder{
posX: 100,
posY: 100,
spacing: HorizontalSpacing,
varTypes: map[string]string{
"Items": "List of Demo.Item",
},
}

id := fb.addListOperationAction(&ast.ListOperationStmt{
OutputVariable: "ExistingItem",
Operation: ast.ListOpFind,
InputVariable: "Items",
Condition: &ast.BinaryExpr{
Left: &ast.IdentifierExpr{Name: "Code"},
Operator: "=",
Right: &ast.AttributePathExpr{
Variable: "IteratorItem",
Path: []string{"ExternalCode"},
},
},
})
if id == "" || len(fb.objects) != 1 {
t.Fatalf("expected one list operation activity, got id=%q objects=%d", id, len(fb.objects))
}

activity, ok := fb.objects[0].(*microflows.ActionActivity)
if !ok {
t.Fatalf("object type = %T, want *microflows.ActionActivity", fb.objects[0])
}
action, ok := activity.Action.(*microflows.ListOperationAction)
if !ok {
t.Fatalf("action type = %T, want *microflows.ListOperationAction", activity.Action)
}
op, ok := action.Operation.(*microflows.FindByAttributeOperation)
if !ok {
t.Fatalf("operation type = %T, want *microflows.FindByAttributeOperation", action.Operation)
}
if op.Attribute != "Demo.Item.Code" {
t.Fatalf("Attribute = %q, want Demo.Item.Code", op.Attribute)
}
if op.Expression != "$IteratorItem/ExternalCode" {
t.Fatalf("Expression = %q, want $IteratorItem/ExternalCode", op.Expression)
}
}

func TestCallMicroflowUnknownResultTypeStillDeclaresVariable(t *testing.T) {
fb := &flowBuilder{
varTypes: map[string]string{"Result": "Old.ModuleEntity"},
Expand Down
80 changes: 72 additions & 8 deletions mdl/executor/cmd_microflows_builder_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,16 +460,24 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID
ListVariable: s.InputVariable,
}
case ast.ListOpFind:
operation = &microflows.FindOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Expression: fb.exprToString(s.Condition),
if op := fb.listAttributeOperation(s, false); op != nil {
operation = op
} else {
operation = &microflows.FindOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Expression: fb.exprToString(s.Condition),
}
}
case ast.ListOpFilter:
operation = &microflows.FilterOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Expression: fb.exprToString(s.Condition),
if op := fb.listAttributeOperation(s, true); op != nil {
operation = op
} else {
operation = &microflows.FilterOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Expression: fb.exprToString(s.Condition),
}
}
case ast.ListOpSort:
// Resolve entity type from input variable for qualified attribute names
Expand Down Expand Up @@ -591,6 +599,62 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID
return activity.ID
}

func (fb *flowBuilder) listAttributeOperation(s *ast.ListOperationStmt, filter bool) microflows.ListOperation {
binary, ok := s.Condition.(*ast.BinaryExpr)
if !ok || binary.Operator != "=" {
return nil
}
fieldName, ok := listOperationFieldName(binary.Left)
if !ok || fieldName == "" {
return nil
}
expression := fb.exprToString(binary.Right)
if expression == "" {
return nil
}

attributeName, associationName := fb.resolveListOperationMember(s.InputVariable, fieldName)
if filter {
return &microflows.FilterByAttributeOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Attribute: attributeName,
Association: associationName,
Expression: expression,
}
}
return &microflows.FindByAttributeOperation{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ListVariable: s.InputVariable,
Attribute: attributeName,
Association: associationName,
Expression: expression,
}
}

func listOperationFieldName(expr ast.Expression) (string, bool) {
switch e := expr.(type) {
case *ast.IdentifierExpr:
return e.Name, true
case *ast.QualifiedNameExpr:
return e.QualifiedName.String(), true
default:
return "", false
}
}

func (fb *flowBuilder) resolveListOperationMember(listVariable, memberName string) (attributeName, associationName string) {
entityQN := ""
if fb.varTypes != nil {
if listType := fb.varTypes[listVariable]; strings.HasPrefix(listType, "List of ") {
entityQN = strings.TrimPrefix(listType, "List of ")
}
}
memberChange := &microflows.MemberChange{}
fb.resolveMemberChange(memberChange, memberName, entityQN)
return memberChange.AttributeQualifiedName, memberChange.AssociationQualifiedName
}

// addAggregateListAction creates aggregate operations like COUNT, SUM, AVERAGE, etc.
func (fb *flowBuilder) addAggregateListAction(s *ast.AggregateListStmt) model.ID {
var function microflows.AggregateFunction
Expand Down
62 changes: 62 additions & 0 deletions sdk/mpr/writer_listoperation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0

package mpr

import (
"testing"

"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/microflows"
"go.mongodb.org/mongo-driver/bson"
)

func TestSerializeListOperation_FindByAttribute(t *testing.T) {
doc := serializeListOperation(&microflows.FindByAttributeOperation{
BaseElement: model.BaseElement{ID: "operation-id"},
ListVariable: "Items",
Attribute: "Demo.Item.Code",
Expression: "$IteratorItem/ExternalCode",
})
fields := listOperationDocMap(doc)

if got := fields["$Type"]; got != "Microflows$Find" {
t.Fatalf("$Type = %v, want Microflows$Find", got)
}
if got := fields["Attribute"]; got != "Demo.Item.Code" {
t.Fatalf("Attribute = %v, want Demo.Item.Code", got)
}
if got := fields["Expression"]; got != "$IteratorItem/ExternalCode" {
t.Fatalf("Expression = %v, want $IteratorItem/ExternalCode", got)
}
if got := fields["ListName"]; got != "Items" {
t.Fatalf("ListName = %v, want Items", got)
}
}

func TestSerializeListOperation_FilterByAssociation(t *testing.T) {
doc := serializeListOperation(&microflows.FilterByAttributeOperation{
BaseElement: model.BaseElement{ID: "operation-id"},
ListVariable: "Items",
Association: "Demo.Item_Category",
Expression: "$Category",
})
fields := listOperationDocMap(doc)

if got := fields["$Type"]; got != "Microflows$Filter" {
t.Fatalf("$Type = %v, want Microflows$Filter", got)
}
if got := fields["Association"]; got != "Demo.Item_Category" {
t.Fatalf("Association = %v, want Demo.Item_Category", got)
}
if got := fields["Expression"]; got != "$Category" {
t.Fatalf("Expression = %v, want $Category", got)
}
}

func listOperationDocMap(doc bson.D) map[string]any {
fields := make(map[string]any, len(doc))
for _, elem := range doc {
fields[elem.Key] = elem.Value
}
return fields
}
18 changes: 18 additions & 0 deletions sdk/mpr/writer_microflow_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,24 @@ func serializeListOperation(op microflows.ListOperation) bson.D {
{Key: "Expression", Value: o.Expression},
{Key: "ListName", Value: o.ListVariable}, // storageName: ListName
}
case *microflows.FindByAttributeOperation:
return bson.D{
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
{Key: "$Type", Value: "Microflows$Find"},
{Key: "Association", Value: o.Association},
{Key: "Attribute", Value: o.Attribute},
{Key: "Expression", Value: o.Expression},
{Key: "ListName", Value: o.ListVariable},
}
case *microflows.FilterByAttributeOperation:
return bson.D{
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
{Key: "$Type", Value: "Microflows$Filter"},
{Key: "Association", Value: o.Association},
{Key: "Attribute", Value: o.Attribute},
{Key: "Expression", Value: o.Expression},
{Key: "ListName", Value: o.ListVariable},
}
case *microflows.SortOperation:
// Build sorting items
sortings := bson.A{int32(3)} // Array with items marker
Expand Down