diff --git a/mdl-examples/bug-tests/341-microflow-call-bson-field-order.mdl b/mdl-examples/bug-tests/341-microflow-call-bson-field-order.mdl new file mode 100644 index 00000000..b4d75fac --- /dev/null +++ b/mdl-examples/bug-tests/341-microflow-call-bson-field-order.mdl @@ -0,0 +1,61 @@ +-- ============================================================================ +-- Bug #341: Microflow call BSON field order produced CE0117 in Studio Pro +-- ============================================================================ +-- +-- Symptom (before fix): +-- `mxcli exec` could roundtrip the textual MDL of a microflow call +-- (`$Result = call microflow Module.Target (Param = $Var);`) unchanged, +-- but Studio Pro flagged the resulting activity with +-- [CE0117] expression error on the call activity +-- even though the MDL source had not been edited. The cause was BSON +-- field order: +-- - The writer emitted result-variable fields BEFORE the nested +-- `MicroflowCall` document. +-- - Inside `MicroflowCall`, `QueueSettings` was emitted BEFORE +-- `ParameterMappings`. +-- - Each `MicroflowCallParameterMapping` was emitted with `Parameter` +-- BEFORE `Argument`. +-- Studio Pro's expression validator depends on this order being stable +-- to resolve parameter references. +-- +-- After fix: +-- `serializeMicroflowAction` now emits: +-- - `MicroflowCallAction`: $ID, $Type, ErrorHandlingType, MicroflowCall, +-- ResultVariableName, UseReturnVariable (call payload before result fields). +-- - `MicroflowCall`: $ID, $Type, Microflow, ParameterMappings, QueueSettings. +-- - `MicroflowCallParameterMapping`: $ID, $Type, Argument, Parameter. +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/341-microflow-call-bson-field-order.mdl -p app.mpr +-- mxcli -p app.mpr -c "describe microflow BugTest341.MF_Caller" +-- `mx check` against the resulting MPR must report 0 errors and the +-- call activity must not surface CE0117 in Studio Pro. +-- ============================================================================ + +create module BugTest341; + +create entity BugTest341.Item ( + Name : string(100) +); +/ + +-- Callee with one parameter, exercising the parameter-mapping serialization. +create microflow BugTest341.MF_RenameItem ( + $Item: BugTest341.Item, + $NewName: string +) +begin + change $Item (Name = $NewName); +end; +/ + +-- Caller — produces a MicroflowCallAction whose nested MicroflowCall +-- contains ParameterMappings. Field order in the serialized BSON must +-- match Studio Pro's expectation. +create microflow BugTest341.MF_Caller ( + $Item: BugTest341.Item +) +begin + call microflow BugTest341.MF_RenameItem (Item = $Item, NewName = 'updated'); +end; +/ diff --git a/sdk/mpr/microflow_call_writer_test.go b/sdk/mpr/microflow_call_writer_test.go new file mode 100644 index 00000000..4df11fda --- /dev/null +++ b/sdk/mpr/microflow_call_writer_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mpr + +import ( + "reflect" + "testing" + + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" + "go.mongodb.org/mongo-driver/bson" +) + +func TestMicroflowCallAction_WritesStableFieldOrder(t *testing.T) { + action := µflows.MicroflowCallAction{ + BaseElement: model.BaseElement{ID: "action-id"}, + ErrorHandlingType: microflows.ErrorHandlingTypeRollback, + MicroflowCall: µflows.MicroflowCall{ + BaseElement: model.BaseElement{ID: "call-id"}, + Microflow: "Demo.UpdateRecord", + ParameterMappings: []*microflows.MicroflowCallParameterMapping{ + { + BaseElement: model.BaseElement{ID: "mapping-id"}, + Argument: "$Record/Name", + Parameter: "Demo.UpdateRecord.Name", + }, + }, + }, + UseReturnVariable: true, + } + + doc := serializeMicroflowAction(action) + assertBSONKeys(t, doc, []string{ + "$ID", + "$Type", + "ErrorHandlingType", + "MicroflowCall", + "ResultVariableName", + "UseReturnVariable", + }) + + callDoc, ok := bsonValue(doc, "MicroflowCall").(bson.D) + if !ok { + t.Fatalf("MicroflowCall type = %T, want bson.D", bsonValue(doc, "MicroflowCall")) + } + assertBSONKeys(t, callDoc, []string{ + "$ID", + "$Type", + "Microflow", + "ParameterMappings", + "QueueSettings", + }) + + mappings, ok := bsonValue(callDoc, "ParameterMappings").(bson.A) + if !ok || len(mappings) != 2 { + t.Fatalf("ParameterMappings = %#v, want marker plus one mapping", bsonValue(callDoc, "ParameterMappings")) + } + mappingDoc, ok := mappings[1].(bson.D) + if !ok { + t.Fatalf("mapping type = %T, want bson.D", mappings[1]) + } + assertBSONKeys(t, mappingDoc, []string{ + "$ID", + "$Type", + "Argument", + "Parameter", + }) +} + +func assertBSONKeys(t *testing.T, doc bson.D, want []string) { + t.Helper() + + var got []string + for _, elem := range doc { + got = append(got, elem.Key) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("BSON keys = %#v, want %#v", got, want) + } +} + +func bsonValue(doc bson.D, key string) any { + for _, elem := range doc { + if elem.Key == key { + return elem.Value + } + } + return nil +} diff --git a/sdk/mpr/writer_microflow_actions.go b/sdk/mpr/writer_microflow_actions.go index 25c7ab08..304baed9 100644 --- a/sdk/mpr/writer_microflow_actions.go +++ b/sdk/mpr/writer_microflow_actions.go @@ -202,8 +202,6 @@ func serializeMicroflowAction(action microflows.MicroflowAction) bson.D { {Key: "$ID", Value: idToBsonBinary(string(a.ID))}, {Key: "$Type", Value: "Microflows$MicroflowCallAction"}, {Key: "ErrorHandlingType", Value: stringOrDefault(string(a.ErrorHandlingType), "Rollback")}, - {Key: "ResultVariableName", Value: a.ResultVariableName}, - {Key: "UseReturnVariable", Value: a.UseReturnVariable}, } // Serialize nested MicroflowCall structure if a.MicroflowCall != nil { @@ -211,7 +209,6 @@ func serializeMicroflowAction(action microflows.MicroflowAction) bson.D { {Key: "$ID", Value: idToBsonBinary(string(a.MicroflowCall.ID))}, {Key: "$Type", Value: "Microflows$MicroflowCall"}, {Key: "Microflow", Value: a.MicroflowCall.Microflow}, - {Key: "QueueSettings", Value: nil}, } // Serialize parameter mappings within MicroflowCall if len(a.MicroflowCall.ParameterMappings) > 0 { @@ -221,8 +218,8 @@ func serializeMicroflowAction(action microflows.MicroflowAction) bson.D { mapping := bson.D{ {Key: "$ID", Value: idToBsonBinary(string(pm.ID))}, {Key: "$Type", Value: "Microflows$MicroflowCallParameterMapping"}, - {Key: "Parameter", Value: pm.Parameter}, {Key: "Argument", Value: pm.Argument}, + {Key: "Parameter", Value: pm.Parameter}, } mappings = append(mappings, mapping) } @@ -230,8 +227,13 @@ func serializeMicroflowAction(action microflows.MicroflowAction) bson.D { } else { mfCall = append(mfCall, bson.E{Key: "ParameterMappings", Value: bson.A{int32(2)}}) // Empty array with marker } + mfCall = append(mfCall, bson.E{Key: "QueueSettings", Value: nil}) doc = append(doc, bson.E{Key: "MicroflowCall", Value: mfCall}) } + doc = append(doc, + bson.E{Key: "ResultVariableName", Value: a.ResultVariableName}, + bson.E{Key: "UseReturnVariable", Value: a.UseReturnVariable}, + ) return doc case *microflows.JavaActionCallAction: