88
99 "github.com/mendixlabs/mxcli/model"
1010 "github.com/mendixlabs/mxcli/sdk/microflows"
11+ "github.com/mendixlabs/mxcli/sdk/mpr/version"
1112
1213 "go.mongodb.org/mongo-driver/bson"
1314)
@@ -105,13 +106,19 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
105106 }
106107
107108 // Add Flows array (SequenceFlows and AnnotationFlows go here, not in ObjectCollection)
109+ // The serialized shape depends on the project's Mendix major version.
110+ // Fall back to the project default when no MPR is attached (in-memory tests).
111+ majorVersion := version .DefaultVersion ().MajorVersion
112+ if pv := w .reader .ProjectVersion (); pv != nil {
113+ majorVersion = pv .MajorVersion
114+ }
108115 flows := bson.A {int32 (3 )} // Start with array type marker
109116 if mf .ObjectCollection != nil {
110117 for _ , flow := range mf .ObjectCollection .Flows {
111- flows = append (flows , serializeSequenceFlow (flow ))
118+ flows = append (flows , serializeSequenceFlow (flow , majorVersion ))
112119 }
113120 for _ , af := range mf .ObjectCollection .AnnotationFlows {
114- flows = append (flows , serializeAnnotationFlow (af ))
121+ flows = append (flows , serializeAnnotationFlow (af , majorVersion ))
115122 }
116123 }
117124 doc = append (doc , bson.E {Key : "Flows" , Value : flows })
@@ -133,63 +140,37 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
133140 // Add object collection (without flows - they're in Flows array)
134141 // Parameters go in ObjectCollection.Objects, pass them here
135142 if mf .ObjectCollection != nil {
136- doc = append (doc , bson.E {Key : "ObjectCollection" , Value : serializeMicroflowObjectCollectionWithoutFlows (mf .ObjectCollection , mf .Parameters )})
143+ doc = append (doc , bson.E {Key : "ObjectCollection" , Value : serializeMicroflowObjectCollectionWithoutFlows (mf .ObjectCollection , mf .Parameters , majorVersion )})
137144 }
138145
139- // Add remaining optional fields
140- // ReturnVariableName is "" by default (Studio Pro convention).
141- // Only set a custom name when explicitly specified via "RETURNS xxx AS $VarName".
142- doc = append (doc , bson.E {Key : "ReturnVariableName" , Value : mf .ReturnVariableName })
143- doc = append (doc , bson.E {Key : "StableId" , Value : idToBsonBinary (generateUUID ())})
144- doc = append (doc , bson.E {Key : "Url" , Value : "" })
145- doc = append (doc , bson.E {Key : "UrlSearchParameters" , Value : bson.A {int32 (1 )}})
146+ // ReturnVariableName, StableId, Url, and UrlSearchParameters were added in
147+ // Mendix 10; Mendix 9 projects do not know about these fields and Studio Pro
148+ // raises metamodel errors if they're present.
149+ if majorVersion >= 10 {
150+ // ReturnVariableName is "" by default (Studio Pro convention).
151+ // Only set a custom name when explicitly specified via "RETURNS xxx AS $VarName".
152+ doc = append (doc , bson.E {Key : "ReturnVariableName" , Value : mf .ReturnVariableName })
153+ doc = append (doc , bson.E {Key : "StableId" , Value : idToBsonBinary (generateUUID ())})
154+ doc = append (doc , bson.E {Key : "Url" , Value : "" })
155+ doc = append (doc , bson.E {Key : "UrlSearchParameters" , Value : bson.A {int32 (1 )}})
156+ }
146157 doc = append (doc , bson.E {Key : "WorkflowActionInfo" , Value : nil })
147158
148159 return bson .Marshal (doc )
149160}
150161
151162// serializeSequenceFlow serializes a SequenceFlow to BSON with correct structure.
152- func serializeSequenceFlow (flow * microflows.SequenceFlow ) bson.D {
153- // Serialize CaseValues
154- caseValues := bson.A {int32 (2 )} // Default empty array marker
155- if flow .CaseValue != nil {
156- switch cv := flow .CaseValue .(type ) {
157- case microflows.EnumerationCase :
158- caseValues = bson.A {
159- int32 (2 ),
160- bson.D {
161- {Key : "$ID" , Value : idToBsonBinary (string (cv .ID ))},
162- {Key : "$Type" , Value : "Microflows$EnumerationCase" },
163- {Key : "Value" , Value : cv .Value },
164- },
165- }
166- case * microflows.EnumerationCase :
167- caseValues = bson.A {
168- int32 (2 ),
169- bson.D {
170- {Key : "$ID" , Value : idToBsonBinary (string (cv .ID ))},
171- {Key : "$Type" , Value : "Microflows$EnumerationCase" },
172- {Key : "Value" , Value : cv .Value },
173- },
174- }
175- case microflows.NoCase :
176- caseValues = bson.A {
177- int32 (2 ),
178- bson.D {
179- {Key : "$ID" , Value : idToBsonBinary (string (cv .ID ))},
180- {Key : "$Type" , Value : "Microflows$NoCase" },
181- },
182- }
183- case * microflows.NoCase :
184- caseValues = bson.A {
185- int32 (2 ),
186- bson.D {
187- {Key : "$ID" , Value : idToBsonBinary (string (cv .ID ))},
188- {Key : "$Type" , Value : "Microflows$NoCase" },
189- },
190- }
191- }
192- }
163+ //
164+ // The case value shape is version-specific:
165+ // - Mendix 9: inline `NewCaseValue` document (NoCase for non-decision flows,
166+ // EnumerationCase for decision branches). `CaseValues` is omitted.
167+ // - Mendix 10+: `CaseValues = [marker, case]` where the case is always present
168+ // (at minimum a NoCase object). Studio Pro rejects `CaseValues = [marker]`
169+ // alone with CE0079/CE0773 "condition value must be configured".
170+ func serializeSequenceFlow (flow * microflows.SequenceFlow , majorVersion int ) bson.D {
171+ // Build the case document. Every sequence flow needs a case — NoCase is the
172+ // default when no branch condition has been set.
173+ caseDoc := buildSequenceFlowCase (flow .CaseValue )
193174
194175 originCV := flow .OriginControlVector
195176 if originCV == "" {
@@ -200,26 +181,100 @@ func serializeSequenceFlow(flow *microflows.SequenceFlow) bson.D {
200181 destCV = "0;0"
201182 }
202183
203- return bson.D {
184+ doc := bson.D {
204185 {Key : "$ID" , Value : idToBsonBinary (string (flow .ID ))},
205186 {Key : "$Type" , Value : "Microflows$SequenceFlow" },
206- {Key : "CaseValues" , Value : caseValues },
207- {Key : "DestinationConnectionIndex" , Value : int32 (flow .DestinationConnectionIndex )},
208- {Key : "DestinationPointer" , Value : idToBsonBinary (string (flow .DestinationID ))},
209- {Key : "IsErrorHandler" , Value : flow .IsErrorHandler },
210- {Key : "Line" , Value : bson.D {
211- {Key : "$ID" , Value : idToBsonBinary (generateUUID ())},
212- {Key : "$Type" , Value : "Microflows$BezierCurve" },
213- {Key : "DestinationControlVector" , Value : destCV },
214- {Key : "OriginControlVector" , Value : originCV },
215- }},
216- {Key : "OriginConnectionIndex" , Value : int32 (flow .OriginConnectionIndex )},
217- {Key : "OriginPointer" , Value : idToBsonBinary (string (flow .OriginID ))},
187+ }
188+
189+ if majorVersion <= 9 {
190+ // Legacy Mendix 9 shape:
191+ // - inline NewCaseValue (no CaseValues array)
192+ // - OriginBezierVector / DestinationBezierVector are top-level strings
193+ // (no nested Line: Microflows$BezierCurve document)
194+ doc = append (doc , bson.E {Key : "DestinationBezierVector" , Value : destCV })
195+ doc = append (doc , bson.E {Key : "DestinationConnectionIndex" , Value : int32 (flow .DestinationConnectionIndex )})
196+ doc = append (doc , bson.E {Key : "DestinationPointer" , Value : idToBsonBinary (string (flow .DestinationID ))})
197+ doc = append (doc , bson.E {Key : "IsErrorHandler" , Value : flow .IsErrorHandler })
198+ doc = append (doc , bson.E {Key : "NewCaseValue" , Value : caseDoc })
199+ doc = append (doc , bson.E {Key : "OriginBezierVector" , Value : originCV })
200+ doc = append (doc , bson.E {Key : "OriginConnectionIndex" , Value : int32 (flow .OriginConnectionIndex )})
201+ doc = append (doc , bson.E {Key : "OriginPointer" , Value : idToBsonBinary (string (flow .OriginID ))})
202+ return doc
203+ }
204+
205+ // Modern format (Mx 10+): CaseValues = [marker, caseDoc].
206+ doc = append (doc , bson.E {Key : "CaseValues" , Value : bson.A {int32 (2 ), caseDoc }})
207+ doc = append (doc , bson.E {Key : "DestinationConnectionIndex" , Value : int32 (flow .DestinationConnectionIndex )})
208+ doc = append (doc , bson.E {Key : "DestinationPointer" , Value : idToBsonBinary (string (flow .DestinationID ))})
209+ doc = append (doc , bson.E {Key : "IsErrorHandler" , Value : flow .IsErrorHandler })
210+ doc = append (doc , bson.E {Key : "Line" , Value : bson.D {
211+ {Key : "$ID" , Value : idToBsonBinary (generateUUID ())},
212+ {Key : "$Type" , Value : "Microflows$BezierCurve" },
213+ {Key : "DestinationControlVector" , Value : destCV },
214+ {Key : "OriginControlVector" , Value : originCV },
215+ }})
216+ doc = append (doc , bson.E {Key : "OriginConnectionIndex" , Value : int32 (flow .OriginConnectionIndex )})
217+ doc = append (doc , bson.E {Key : "OriginPointer" , Value : idToBsonBinary (string (flow .OriginID ))})
218+ return doc
219+ }
220+
221+ // buildSequenceFlowCase renders the case document for a sequence flow.
222+ // When no case has been set on the flow, a NoCase document is synthesised —
223+ // Studio Pro requires every SequenceFlow to carry an explicit case object.
224+ func buildSequenceFlowCase (cv microflows.CaseValue ) bson.D {
225+ // Normalise value receivers to pointers so each case is handled once.
226+ switch c := cv .(type ) {
227+ case microflows.EnumerationCase :
228+ cv = & c
229+ case microflows.NoCase :
230+ cv = & c
231+ }
232+
233+ switch c := cv .(type ) {
234+ case * microflows.EnumerationCase :
235+ id := string (c .ID )
236+ if id == "" {
237+ id = generateUUID ()
238+ }
239+ return bson.D {
240+ {Key : "$ID" , Value : idToBsonBinary (id )},
241+ {Key : "$Type" , Value : "Microflows$EnumerationCase" },
242+ {Key : "Value" , Value : c .Value },
243+ }
244+ case * microflows.NoCase :
245+ id := string (c .ID )
246+ if id == "" {
247+ id = generateUUID ()
248+ }
249+ return bson.D {
250+ {Key : "$ID" , Value : idToBsonBinary (id )},
251+ {Key : "$Type" , Value : "Microflows$NoCase" },
252+ }
253+ }
254+ // Default: synthesise a NoCase document with a fresh ID.
255+ return bson.D {
256+ {Key : "$ID" , Value : idToBsonBinary (generateUUID ())},
257+ {Key : "$Type" , Value : "Microflows$NoCase" },
218258 }
219259}
220260
221261// serializeAnnotationFlow serializes an AnnotationFlow to BSON.
222- func serializeAnnotationFlow (af * microflows.AnnotationFlow ) bson.D {
262+ // The line shape is version-specific: Mendix 9 stores OriginBezierVector /
263+ // DestinationBezierVector as top-level strings, while Mendix 10+ nests them
264+ // inside a Microflows$BezierCurve document under `Line`.
265+ func serializeAnnotationFlow (af * microflows.AnnotationFlow , majorVersion int ) bson.D {
266+ if majorVersion <= 9 {
267+ return bson.D {
268+ {Key : "$ID" , Value : idToBsonBinary (string (af .ID ))},
269+ {Key : "$Type" , Value : "Microflows$AnnotationFlow" },
270+ {Key : "DestinationBezierVector" , Value : "0;0" },
271+ {Key : "DestinationConnectionIndex" , Value : int32 (0 )},
272+ {Key : "DestinationPointer" , Value : idToBsonBinary (string (af .DestinationID ))},
273+ {Key : "OriginBezierVector" , Value : "0;0" },
274+ {Key : "OriginConnectionIndex" , Value : int32 (0 )},
275+ {Key : "OriginPointer" , Value : idToBsonBinary (string (af .OriginID ))},
276+ }
277+ }
223278 return bson.D {
224279 {Key : "$ID" , Value : idToBsonBinary (string (af .ID ))},
225280 {Key : "$Type" , Value : "Microflows$AnnotationFlow" },
@@ -238,21 +293,28 @@ func serializeAnnotationFlow(af *microflows.AnnotationFlow) bson.D {
238293
239294// serializeMicroflowParameter serializes a MicroflowParameter to BSON.
240295// Parameters go in ObjectCollection.Objects, not in a separate collection.
241- func serializeMicroflowParameter (p * microflows.MicroflowParameter , posX int ) bson.D {
296+ //
297+ // DefaultValue and IsRequired were introduced in Mendix 10; emitting them on a
298+ // Mendix 9 project trips the Studio Pro metamodel checker, so they are gated.
299+ func serializeMicroflowParameter (p * microflows.MicroflowParameter , posX int , majorVersion int ) bson.D {
242300 // Calculate position based on index - parameters appear at the top of the microflow
243301 relativeMiddlePoint := fmt .Sprintf ("%d;53" , 200 + posX * 100 )
244302
245303 doc := bson.D {
246304 {Key : "$ID" , Value : idToBsonBinary (string (p .ID ))},
247305 {Key : "$Type" , Value : "Microflows$MicroflowParameter" },
248- {Key : "DefaultValue" , Value : "" },
249- {Key : "Documentation" , Value : p .Documentation },
250- {Key : "HasVariableNameBeenChanged" , Value : false },
251- {Key : "IsRequired" , Value : true },
252- {Key : "Name" , Value : p .Name },
253- {Key : "RelativeMiddlePoint" , Value : relativeMiddlePoint },
254- {Key : "Size" , Value : "30;30" },
255306 }
307+ if majorVersion >= 10 {
308+ doc = append (doc , bson.E {Key : "DefaultValue" , Value : "" })
309+ }
310+ doc = append (doc , bson.E {Key : "Documentation" , Value : p .Documentation })
311+ doc = append (doc , bson.E {Key : "HasVariableNameBeenChanged" , Value : false })
312+ if majorVersion >= 10 {
313+ doc = append (doc , bson.E {Key : "IsRequired" , Value : true })
314+ }
315+ doc = append (doc , bson.E {Key : "Name" , Value : p .Name })
316+ doc = append (doc , bson.E {Key : "RelativeMiddlePoint" , Value : relativeMiddlePoint })
317+ doc = append (doc , bson.E {Key : "Size" , Value : "30;30" })
256318 if p .Type != nil {
257319 doc = append (doc , bson.E {Key : "VariableType" , Value : serializeMicroflowDataType (p .Type )})
258320 }
@@ -350,13 +412,13 @@ func serializeMicroflowDataType(dt microflows.DataType) bson.D {
350412
351413// serializeMicroflowObjectCollectionWithoutFlows serializes the object collection to BSON (flows are in separate Flows array).
352414// Parameters are also included in the Objects array.
353- func serializeMicroflowObjectCollectionWithoutFlows (oc * microflows.MicroflowObjectCollection , params []* microflows.MicroflowParameter ) bson.D {
415+ func serializeMicroflowObjectCollectionWithoutFlows (oc * microflows.MicroflowObjectCollection , params []* microflows.MicroflowParameter , majorVersion int ) bson.D {
354416 // Start with array type marker, then serialize objects (NOT flows)
355417 objects := bson.A {int32 (3 )} // Array type marker
356418
357419 // Add parameters first (they appear at the top of the microflow)
358420 for i , p := range params {
359- objects = append (objects , serializeMicroflowParameter (p , i ))
421+ objects = append (objects , serializeMicroflowParameter (p , i , majorVersion ))
360422 }
361423
362424 // Add regular microflow objects
@@ -404,16 +466,21 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
404466 }
405467
406468 case * microflows.EndEvent :
469+ // Pristine EndEvents always carry `ReturnValue` (empty string for void
470+ // microflows; expression + "\n" when a value is returned). Omitting it
471+ // diverges from the pristine key set on Mx 9 roundtrips.
472+ returnValue := ""
473+ if o .ReturnValue != "" {
474+ returnValue = o .ReturnValue + "\n "
475+ }
407476 doc := bson.D {
408477 {Key : "$ID" , Value : idToBsonBinary (string (o .ID ))},
409478 {Key : "$Type" , Value : "Microflows$EndEvent" },
410479 {Key : "Documentation" , Value : "" },
411480 {Key : "RelativeMiddlePoint" , Value : pointToString (o .Position )},
481+ {Key : "ReturnValue" , Value : returnValue },
482+ {Key : "Size" , Value : sizeToString (o .Size )},
412483 }
413- if o .ReturnValue != "" {
414- doc = append (doc , bson.E {Key : "ReturnValue" , Value : o .ReturnValue + "\n " })
415- }
416- doc = append (doc , bson.E {Key : "Size" , Value : sizeToString (o .Size )})
417484 return doc
418485
419486 case * microflows.ErrorEvent :
@@ -450,6 +517,7 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
450517 {Key : "$ID" , Value : idToBsonBinary (string (o .ID ))},
451518 {Key : "$Type" , Value : "Microflows$ExclusiveSplit" },
452519 {Key : "Caption" , Value : o .Caption },
520+ {Key : "Documentation" , Value : o .Documentation },
453521 {Key : "ErrorHandlingType" , Value : string (o .ErrorHandlingType )},
454522 {Key : "RelativeMiddlePoint" , Value : pointToString (o .Position )},
455523 {Key : "Size" , Value : sizeToString (o .Size )},
0 commit comments