Skip to content
Merged
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
38 changes: 38 additions & 0 deletions mdl-examples/bug-tests/261-mx9-microflow-roundtrip.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- ============================================================================
-- Bug #261: Mx 9 microflow roundtrip keys gated by project version
-- ============================================================================
--
-- Symptom (before fix):
-- Writing a microflow to a Mendix 9 project unconditionally emitted
-- ReturnVariableName / StableId / Url / UrlSearchParameters on the
-- Microflow document and DefaultValue / IsRequired on its parameters.
-- Studio Pro on Mx 9 treats those as unknown metamodel keys and raises
-- schema validation errors on open.
--
-- Similarly, SequenceFlow and AnnotationFlow were always emitted with
-- the Mx 10+ shape (CaseValues array, Line document), so a Mx 9 project
-- re-parsed them as malformed and dropped case values / bezier vectors.
--
-- After fix:
-- The writer reads the project's major version from the reader and emits
-- the matching key set:
-- - Mx 9: legacy NewCaseValue + top-level {Origin,Destination}BezierVector
-- - Mx 10+: modern CaseValues + Line: Microflows$BezierCurve
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/261-mx9-microflow-roundtrip.mdl -p mx9-app.mpr
-- Open in Studio Pro 9 — project must load without metamodel errors.
-- ============================================================================

create module BugTest261;

create microflow BugTest261.MF_Demo (
$name: string
)
returns string as $result
begin
declare $result string = empty;
set $result = $name;
return $result;
end;
/
226 changes: 147 additions & 79 deletions sdk/mpr/writer_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/microflows"
"github.com/mendixlabs/mxcli/sdk/mpr/version"

"go.mongodb.org/mongo-driver/bson"
)
Expand Down Expand Up @@ -105,13 +106,19 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
}

// Add Flows array (SequenceFlows and AnnotationFlows go here, not in ObjectCollection)
// The serialized shape depends on the project's Mendix major version.
// Fall back to the project default when no MPR is attached (in-memory tests).
majorVersion := version.DefaultVersion().MajorVersion
if pv := w.reader.ProjectVersion(); pv != nil {
majorVersion = pv.MajorVersion
}
flows := bson.A{int32(3)} // Start with array type marker
if mf.ObjectCollection != nil {
for _, flow := range mf.ObjectCollection.Flows {
flows = append(flows, serializeSequenceFlow(flow))
flows = append(flows, serializeSequenceFlow(flow, majorVersion))
}
for _, af := range mf.ObjectCollection.AnnotationFlows {
flows = append(flows, serializeAnnotationFlow(af))
flows = append(flows, serializeAnnotationFlow(af, majorVersion))
}
}
doc = append(doc, bson.E{Key: "Flows", Value: flows})
Expand All @@ -133,63 +140,37 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
// Add object collection (without flows - they're in Flows array)
// Parameters go in ObjectCollection.Objects, pass them here
if mf.ObjectCollection != nil {
doc = append(doc, bson.E{Key: "ObjectCollection", Value: serializeMicroflowObjectCollectionWithoutFlows(mf.ObjectCollection, mf.Parameters)})
doc = append(doc, bson.E{Key: "ObjectCollection", Value: serializeMicroflowObjectCollectionWithoutFlows(mf.ObjectCollection, mf.Parameters, majorVersion)})
}

// Add remaining optional fields
// ReturnVariableName is "" by default (Studio Pro convention).
// Only set a custom name when explicitly specified via "RETURNS xxx AS $VarName".
doc = append(doc, bson.E{Key: "ReturnVariableName", Value: mf.ReturnVariableName})
doc = append(doc, bson.E{Key: "StableId", Value: idToBsonBinary(generateUUID())})
doc = append(doc, bson.E{Key: "Url", Value: ""})
doc = append(doc, bson.E{Key: "UrlSearchParameters", Value: bson.A{int32(1)}})
// ReturnVariableName, StableId, Url, and UrlSearchParameters were added in
// Mendix 10; Mendix 9 projects do not know about these fields and Studio Pro
// raises metamodel errors if they're present.
if majorVersion >= 10 {
// ReturnVariableName is "" by default (Studio Pro convention).
// Only set a custom name when explicitly specified via "RETURNS xxx AS $VarName".
doc = append(doc, bson.E{Key: "ReturnVariableName", Value: mf.ReturnVariableName})
doc = append(doc, bson.E{Key: "StableId", Value: idToBsonBinary(generateUUID())})
doc = append(doc, bson.E{Key: "Url", Value: ""})
doc = append(doc, bson.E{Key: "UrlSearchParameters", Value: bson.A{int32(1)}})
}
doc = append(doc, bson.E{Key: "WorkflowActionInfo", Value: nil})

return bson.Marshal(doc)
}

// serializeSequenceFlow serializes a SequenceFlow to BSON with correct structure.
func serializeSequenceFlow(flow *microflows.SequenceFlow) bson.D {
// Serialize CaseValues
caseValues := bson.A{int32(2)} // Default empty array marker
if flow.CaseValue != nil {
switch cv := flow.CaseValue.(type) {
case microflows.EnumerationCase:
caseValues = bson.A{
int32(2),
bson.D{
{Key: "$ID", Value: idToBsonBinary(string(cv.ID))},
{Key: "$Type", Value: "Microflows$EnumerationCase"},
{Key: "Value", Value: cv.Value},
},
}
case *microflows.EnumerationCase:
caseValues = bson.A{
int32(2),
bson.D{
{Key: "$ID", Value: idToBsonBinary(string(cv.ID))},
{Key: "$Type", Value: "Microflows$EnumerationCase"},
{Key: "Value", Value: cv.Value},
},
}
case microflows.NoCase:
caseValues = bson.A{
int32(2),
bson.D{
{Key: "$ID", Value: idToBsonBinary(string(cv.ID))},
{Key: "$Type", Value: "Microflows$NoCase"},
},
}
case *microflows.NoCase:
caseValues = bson.A{
int32(2),
bson.D{
{Key: "$ID", Value: idToBsonBinary(string(cv.ID))},
{Key: "$Type", Value: "Microflows$NoCase"},
},
}
}
}
//
// The case value shape is version-specific:
// - Mendix 9: inline `NewCaseValue` document (NoCase for non-decision flows,
// EnumerationCase for decision branches). `CaseValues` is omitted.
// - Mendix 10+: `CaseValues = [marker, case]` where the case is always present
// (at minimum a NoCase object). Studio Pro rejects `CaseValues = [marker]`
// alone with CE0079/CE0773 "condition value must be configured".
func serializeSequenceFlow(flow *microflows.SequenceFlow, majorVersion int) bson.D {
// Build the case document. Every sequence flow needs a case — NoCase is the
// default when no branch condition has been set.
caseDoc := buildSequenceFlowCase(flow.CaseValue)

originCV := flow.OriginControlVector
if originCV == "" {
Expand All @@ -200,26 +181,100 @@ func serializeSequenceFlow(flow *microflows.SequenceFlow) bson.D {
destCV = "0;0"
}

return bson.D{
doc := bson.D{
{Key: "$ID", Value: idToBsonBinary(string(flow.ID))},
{Key: "$Type", Value: "Microflows$SequenceFlow"},
{Key: "CaseValues", Value: caseValues},
{Key: "DestinationConnectionIndex", Value: int32(flow.DestinationConnectionIndex)},
{Key: "DestinationPointer", Value: idToBsonBinary(string(flow.DestinationID))},
{Key: "IsErrorHandler", Value: flow.IsErrorHandler},
{Key: "Line", Value: bson.D{
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
{Key: "$Type", Value: "Microflows$BezierCurve"},
{Key: "DestinationControlVector", Value: destCV},
{Key: "OriginControlVector", Value: originCV},
}},
{Key: "OriginConnectionIndex", Value: int32(flow.OriginConnectionIndex)},
{Key: "OriginPointer", Value: idToBsonBinary(string(flow.OriginID))},
}

if majorVersion <= 9 {
// Legacy Mendix 9 shape:
// - inline NewCaseValue (no CaseValues array)
// - OriginBezierVector / DestinationBezierVector are top-level strings
// (no nested Line: Microflows$BezierCurve document)
doc = append(doc, bson.E{Key: "DestinationBezierVector", Value: destCV})
doc = append(doc, bson.E{Key: "DestinationConnectionIndex", Value: int32(flow.DestinationConnectionIndex)})
doc = append(doc, bson.E{Key: "DestinationPointer", Value: idToBsonBinary(string(flow.DestinationID))})
doc = append(doc, bson.E{Key: "IsErrorHandler", Value: flow.IsErrorHandler})
doc = append(doc, bson.E{Key: "NewCaseValue", Value: caseDoc})
doc = append(doc, bson.E{Key: "OriginBezierVector", Value: originCV})
doc = append(doc, bson.E{Key: "OriginConnectionIndex", Value: int32(flow.OriginConnectionIndex)})
doc = append(doc, bson.E{Key: "OriginPointer", Value: idToBsonBinary(string(flow.OriginID))})
return doc
}

// Modern format (Mx 10+): CaseValues = [marker, caseDoc].
doc = append(doc, bson.E{Key: "CaseValues", Value: bson.A{int32(2), caseDoc}})
doc = append(doc, bson.E{Key: "DestinationConnectionIndex", Value: int32(flow.DestinationConnectionIndex)})
doc = append(doc, bson.E{Key: "DestinationPointer", Value: idToBsonBinary(string(flow.DestinationID))})
doc = append(doc, bson.E{Key: "IsErrorHandler", Value: flow.IsErrorHandler})
doc = append(doc, bson.E{Key: "Line", Value: bson.D{
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
{Key: "$Type", Value: "Microflows$BezierCurve"},
{Key: "DestinationControlVector", Value: destCV},
{Key: "OriginControlVector", Value: originCV},
}})
doc = append(doc, bson.E{Key: "OriginConnectionIndex", Value: int32(flow.OriginConnectionIndex)})
doc = append(doc, bson.E{Key: "OriginPointer", Value: idToBsonBinary(string(flow.OriginID))})
return doc
}

// buildSequenceFlowCase renders the case document for a sequence flow.
// When no case has been set on the flow, a NoCase document is synthesised —
// Studio Pro requires every SequenceFlow to carry an explicit case object.
func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
// Normalise value receivers to pointers so each case is handled once.
switch c := cv.(type) {
case microflows.EnumerationCase:
cv = &c
case microflows.NoCase:
cv = &c
}

switch c := cv.(type) {
case *microflows.EnumerationCase:
id := string(c.ID)
if id == "" {
id = generateUUID()
}
return bson.D{
{Key: "$ID", Value: idToBsonBinary(id)},
{Key: "$Type", Value: "Microflows$EnumerationCase"},
{Key: "Value", Value: c.Value},
}
case *microflows.NoCase:
id := string(c.ID)
if id == "" {
id = generateUUID()
}
return bson.D{
{Key: "$ID", Value: idToBsonBinary(id)},
{Key: "$Type", Value: "Microflows$NoCase"},
}
}
// Default: synthesise a NoCase document with a fresh ID.
return bson.D{
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
{Key: "$Type", Value: "Microflows$NoCase"},
}
}

// serializeAnnotationFlow serializes an AnnotationFlow to BSON.
func serializeAnnotationFlow(af *microflows.AnnotationFlow) bson.D {
// The line shape is version-specific: Mendix 9 stores OriginBezierVector /
// DestinationBezierVector as top-level strings, while Mendix 10+ nests them
// inside a Microflows$BezierCurve document under `Line`.
func serializeAnnotationFlow(af *microflows.AnnotationFlow, majorVersion int) bson.D {
if majorVersion <= 9 {
return bson.D{
{Key: "$ID", Value: idToBsonBinary(string(af.ID))},
{Key: "$Type", Value: "Microflows$AnnotationFlow"},
{Key: "DestinationBezierVector", Value: "0;0"},
{Key: "DestinationConnectionIndex", Value: int32(0)},
{Key: "DestinationPointer", Value: idToBsonBinary(string(af.DestinationID))},
{Key: "OriginBezierVector", Value: "0;0"},
{Key: "OriginConnectionIndex", Value: int32(0)},
{Key: "OriginPointer", Value: idToBsonBinary(string(af.OriginID))},
}
}
return bson.D{
{Key: "$ID", Value: idToBsonBinary(string(af.ID))},
{Key: "$Type", Value: "Microflows$AnnotationFlow"},
Expand All @@ -238,21 +293,28 @@ func serializeAnnotationFlow(af *microflows.AnnotationFlow) bson.D {

// serializeMicroflowParameter serializes a MicroflowParameter to BSON.
// Parameters go in ObjectCollection.Objects, not in a separate collection.
func serializeMicroflowParameter(p *microflows.MicroflowParameter, posX int) bson.D {
//
// DefaultValue and IsRequired were introduced in Mendix 10; emitting them on a
// Mendix 9 project trips the Studio Pro metamodel checker, so they are gated.
func serializeMicroflowParameter(p *microflows.MicroflowParameter, posX int, majorVersion int) bson.D {
// Calculate position based on index - parameters appear at the top of the microflow
relativeMiddlePoint := fmt.Sprintf("%d;53", 200+posX*100)

doc := bson.D{
{Key: "$ID", Value: idToBsonBinary(string(p.ID))},
{Key: "$Type", Value: "Microflows$MicroflowParameter"},
{Key: "DefaultValue", Value: ""},
{Key: "Documentation", Value: p.Documentation},
{Key: "HasVariableNameBeenChanged", Value: false},
{Key: "IsRequired", Value: true},
{Key: "Name", Value: p.Name},
{Key: "RelativeMiddlePoint", Value: relativeMiddlePoint},
{Key: "Size", Value: "30;30"},
}
if majorVersion >= 10 {
doc = append(doc, bson.E{Key: "DefaultValue", Value: ""})
}
doc = append(doc, bson.E{Key: "Documentation", Value: p.Documentation})
doc = append(doc, bson.E{Key: "HasVariableNameBeenChanged", Value: false})
if majorVersion >= 10 {
doc = append(doc, bson.E{Key: "IsRequired", Value: true})
}
doc = append(doc, bson.E{Key: "Name", Value: p.Name})
doc = append(doc, bson.E{Key: "RelativeMiddlePoint", Value: relativeMiddlePoint})
doc = append(doc, bson.E{Key: "Size", Value: "30;30"})
if p.Type != nil {
doc = append(doc, bson.E{Key: "VariableType", Value: serializeMicroflowDataType(p.Type)})
}
Expand Down Expand Up @@ -350,13 +412,13 @@ func serializeMicroflowDataType(dt microflows.DataType) bson.D {

// serializeMicroflowObjectCollectionWithoutFlows serializes the object collection to BSON (flows are in separate Flows array).
// Parameters are also included in the Objects array.
func serializeMicroflowObjectCollectionWithoutFlows(oc *microflows.MicroflowObjectCollection, params []*microflows.MicroflowParameter) bson.D {
func serializeMicroflowObjectCollectionWithoutFlows(oc *microflows.MicroflowObjectCollection, params []*microflows.MicroflowParameter, majorVersion int) bson.D {
// Start with array type marker, then serialize objects (NOT flows)
objects := bson.A{int32(3)} // Array type marker

// Add parameters first (they appear at the top of the microflow)
for i, p := range params {
objects = append(objects, serializeMicroflowParameter(p, i))
objects = append(objects, serializeMicroflowParameter(p, i, majorVersion))
}

// Add regular microflow objects
Expand Down Expand Up @@ -404,16 +466,21 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
}

case *microflows.EndEvent:
// Pristine EndEvents always carry `ReturnValue` (empty string for void
// microflows; expression + "\n" when a value is returned). Omitting it
// diverges from the pristine key set on Mx 9 roundtrips.
returnValue := ""
if o.ReturnValue != "" {
returnValue = o.ReturnValue + "\n"
}
doc := bson.D{
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
{Key: "$Type", Value: "Microflows$EndEvent"},
{Key: "Documentation", Value: ""},
{Key: "RelativeMiddlePoint", Value: pointToString(o.Position)},
{Key: "ReturnValue", Value: returnValue},
{Key: "Size", Value: sizeToString(o.Size)},
}
if o.ReturnValue != "" {
doc = append(doc, bson.E{Key: "ReturnValue", Value: o.ReturnValue + "\n"})
}
doc = append(doc, bson.E{Key: "Size", Value: sizeToString(o.Size)})
return doc

case *microflows.ErrorEvent:
Expand Down Expand Up @@ -450,6 +517,7 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
{Key: "$Type", Value: "Microflows$ExclusiveSplit"},
{Key: "Caption", Value: o.Caption},
{Key: "Documentation", Value: o.Documentation},
{Key: "ErrorHandlingType", Value: string(o.ErrorHandlingType)},
{Key: "RelativeMiddlePoint", Value: pointToString(o.Position)},
{Key: "Size", Value: sizeToString(o.Size)},
Expand Down
Loading
Loading