Skip to content

Commit 457b70e

Browse files
hjothaclaude
authored andcommitted
fix: gate Mx 9 microflow roundtrip keys by project version
Mendix 9 microflows reject ReturnVariableName/StableId/Url/UrlSearchParameters, DefaultValue/IsRequired on parameters, and require NewCaseValue plus Origin/DestinationBezierVector on sequence flows. Detect the project's major version and emit the matching key set; always include Documentation on ExclusiveSplit and ReturnValue on EndEvent so pristine BSON key sets match after roundtrip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent cf6860c commit 457b70e

1 file changed

Lines changed: 156 additions & 79 deletions

File tree

sdk/mpr/writer_microflow.go

Lines changed: 156 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,18 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
105105
}
106106

107107
// Add Flows array (SequenceFlows and AnnotationFlows go here, not in ObjectCollection)
108+
// The serialized shape depends on the project's Mendix major version.
109+
majorVersion := 11 // modern default when version metadata is unavailable (e.g. in-memory tests)
110+
if pv := w.reader.ProjectVersion(); pv != nil {
111+
majorVersion = pv.MajorVersion
112+
}
108113
flows := bson.A{int32(3)} // Start with array type marker
109114
if mf.ObjectCollection != nil {
110115
for _, flow := range mf.ObjectCollection.Flows {
111-
flows = append(flows, serializeSequenceFlow(flow))
116+
flows = append(flows, serializeSequenceFlow(flow, majorVersion))
112117
}
113118
for _, af := range mf.ObjectCollection.AnnotationFlows {
114-
flows = append(flows, serializeAnnotationFlow(af))
119+
flows = append(flows, serializeAnnotationFlow(af, majorVersion))
115120
}
116121
}
117122
doc = append(doc, bson.E{Key: "Flows", Value: flows})
@@ -133,63 +138,37 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
133138
// Add object collection (without flows - they're in Flows array)
134139
// Parameters go in ObjectCollection.Objects, pass them here
135140
if mf.ObjectCollection != nil {
136-
doc = append(doc, bson.E{Key: "ObjectCollection", Value: serializeMicroflowObjectCollectionWithoutFlows(mf.ObjectCollection, mf.Parameters)})
141+
doc = append(doc, bson.E{Key: "ObjectCollection", Value: serializeMicroflowObjectCollectionWithoutFlows(mf.ObjectCollection, mf.Parameters, majorVersion)})
137142
}
138143

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)}})
144+
// ReturnVariableName, StableId, Url, and UrlSearchParameters were added in
145+
// Mendix 10; Mendix 9 projects do not know about these fields and Studio Pro
146+
// raises metamodel errors if they're present.
147+
if majorVersion >= 10 {
148+
// ReturnVariableName is "" by default (Studio Pro convention).
149+
// Only set a custom name when explicitly specified via "RETURNS xxx AS $VarName".
150+
doc = append(doc, bson.E{Key: "ReturnVariableName", Value: mf.ReturnVariableName})
151+
doc = append(doc, bson.E{Key: "StableId", Value: idToBsonBinary(generateUUID())})
152+
doc = append(doc, bson.E{Key: "Url", Value: ""})
153+
doc = append(doc, bson.E{Key: "UrlSearchParameters", Value: bson.A{int32(1)}})
154+
}
146155
doc = append(doc, bson.E{Key: "WorkflowActionInfo", Value: nil})
147156

148157
return bson.Marshal(doc)
149158
}
150159

151160
// 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-
}
161+
//
162+
// The case value shape is version-specific:
163+
// - Mendix 9: inline `NewCaseValue` document (NoCase for non-decision flows,
164+
// EnumerationCase for decision branches). `CaseValues` is omitted.
165+
// - Mendix 10+: `CaseValues = [marker, case]` where the case is always present
166+
// (at minimum a NoCase object). Studio Pro rejects `CaseValues = [marker]`
167+
// alone with CE0079/CE0773 "condition value must be configured".
168+
func serializeSequenceFlow(flow *microflows.SequenceFlow, majorVersion int) bson.D {
169+
// Build the case document. Every sequence flow needs a case — NoCase is the
170+
// default when no branch condition has been set.
171+
caseDoc := buildSequenceFlowCase(flow.CaseValue)
193172

194173
originCV := flow.OriginControlVector
195174
if originCV == "" {
@@ -200,26 +179,111 @@ func serializeSequenceFlow(flow *microflows.SequenceFlow) bson.D {
200179
destCV = "0;0"
201180
}
202181

203-
return bson.D{
182+
doc := bson.D{
204183
{Key: "$ID", Value: idToBsonBinary(string(flow.ID))},
205184
{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))},
185+
}
186+
187+
if majorVersion <= 9 {
188+
// Legacy Mendix 9 shape:
189+
// - inline NewCaseValue (no CaseValues array)
190+
// - OriginBezierVector / DestinationBezierVector are top-level strings
191+
// (no nested Line: Microflows$BezierCurve document)
192+
doc = append(doc, bson.E{Key: "DestinationBezierVector", Value: destCV})
193+
doc = append(doc, bson.E{Key: "DestinationConnectionIndex", Value: int32(flow.DestinationConnectionIndex)})
194+
doc = append(doc, bson.E{Key: "DestinationPointer", Value: idToBsonBinary(string(flow.DestinationID))})
195+
doc = append(doc, bson.E{Key: "IsErrorHandler", Value: flow.IsErrorHandler})
196+
doc = append(doc, bson.E{Key: "NewCaseValue", Value: caseDoc})
197+
doc = append(doc, bson.E{Key: "OriginBezierVector", Value: originCV})
198+
doc = append(doc, bson.E{Key: "OriginConnectionIndex", Value: int32(flow.OriginConnectionIndex)})
199+
doc = append(doc, bson.E{Key: "OriginPointer", Value: idToBsonBinary(string(flow.OriginID))})
200+
return doc
201+
}
202+
203+
// Modern format (Mx 10+): CaseValues = [marker, caseDoc].
204+
doc = append(doc, bson.E{Key: "CaseValues", Value: bson.A{int32(2), caseDoc}})
205+
doc = append(doc, bson.E{Key: "DestinationConnectionIndex", Value: int32(flow.DestinationConnectionIndex)})
206+
doc = append(doc, bson.E{Key: "DestinationPointer", Value: idToBsonBinary(string(flow.DestinationID))})
207+
doc = append(doc, bson.E{Key: "IsErrorHandler", Value: flow.IsErrorHandler})
208+
doc = append(doc, bson.E{Key: "Line", Value: bson.D{
209+
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
210+
{Key: "$Type", Value: "Microflows$BezierCurve"},
211+
{Key: "DestinationControlVector", Value: destCV},
212+
{Key: "OriginControlVector", Value: originCV},
213+
}})
214+
doc = append(doc, bson.E{Key: "OriginConnectionIndex", Value: int32(flow.OriginConnectionIndex)})
215+
doc = append(doc, bson.E{Key: "OriginPointer", Value: idToBsonBinary(string(flow.OriginID))})
216+
return doc
217+
}
218+
219+
// buildSequenceFlowCase renders the case document for a sequence flow.
220+
// When no case has been set on the flow, a NoCase document is synthesised —
221+
// Studio Pro requires every SequenceFlow to carry an explicit case object.
222+
func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
223+
switch c := cv.(type) {
224+
case *microflows.EnumerationCase:
225+
id := string(c.ID)
226+
if id == "" {
227+
id = generateUUID()
228+
}
229+
return bson.D{
230+
{Key: "$ID", Value: idToBsonBinary(id)},
231+
{Key: "$Type", Value: "Microflows$EnumerationCase"},
232+
{Key: "Value", Value: c.Value},
233+
}
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+
case microflows.NoCase:
254+
id := string(c.ID)
255+
if id == "" {
256+
id = generateUUID()
257+
}
258+
return bson.D{
259+
{Key: "$ID", Value: idToBsonBinary(id)},
260+
{Key: "$Type", Value: "Microflows$NoCase"},
261+
}
262+
}
263+
// Default: synthesise a NoCase document with a fresh ID.
264+
return bson.D{
265+
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
266+
{Key: "$Type", Value: "Microflows$NoCase"},
218267
}
219268
}
220269

221270
// serializeAnnotationFlow serializes an AnnotationFlow to BSON.
222-
func serializeAnnotationFlow(af *microflows.AnnotationFlow) bson.D {
271+
// The line shape is version-specific: Mendix 9 stores OriginBezierVector /
272+
// DestinationBezierVector as top-level strings, while Mendix 10+ nests them
273+
// inside a Microflows$BezierCurve document under `Line`.
274+
func serializeAnnotationFlow(af *microflows.AnnotationFlow, majorVersion int) bson.D {
275+
if majorVersion <= 9 {
276+
return bson.D{
277+
{Key: "$ID", Value: idToBsonBinary(string(af.ID))},
278+
{Key: "$Type", Value: "Microflows$AnnotationFlow"},
279+
{Key: "DestinationBezierVector", Value: "0;0"},
280+
{Key: "DestinationConnectionIndex", Value: int32(0)},
281+
{Key: "DestinationPointer", Value: idToBsonBinary(string(af.DestinationID))},
282+
{Key: "OriginBezierVector", Value: "0;0"},
283+
{Key: "OriginConnectionIndex", Value: int32(0)},
284+
{Key: "OriginPointer", Value: idToBsonBinary(string(af.OriginID))},
285+
}
286+
}
223287
return bson.D{
224288
{Key: "$ID", Value: idToBsonBinary(string(af.ID))},
225289
{Key: "$Type", Value: "Microflows$AnnotationFlow"},
@@ -238,21 +302,28 @@ func serializeAnnotationFlow(af *microflows.AnnotationFlow) bson.D {
238302

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

245312
doc := bson.D{
246313
{Key: "$ID", Value: idToBsonBinary(string(p.ID))},
247314
{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"},
255315
}
316+
if majorVersion >= 10 {
317+
doc = append(doc, bson.E{Key: "DefaultValue", Value: ""})
318+
}
319+
doc = append(doc, bson.E{Key: "Documentation", Value: p.Documentation})
320+
doc = append(doc, bson.E{Key: "HasVariableNameBeenChanged", Value: false})
321+
if majorVersion >= 10 {
322+
doc = append(doc, bson.E{Key: "IsRequired", Value: true})
323+
}
324+
doc = append(doc, bson.E{Key: "Name", Value: p.Name})
325+
doc = append(doc, bson.E{Key: "RelativeMiddlePoint", Value: relativeMiddlePoint})
326+
doc = append(doc, bson.E{Key: "Size", Value: "30;30"})
256327
if p.Type != nil {
257328
doc = append(doc, bson.E{Key: "VariableType", Value: serializeMicroflowDataType(p.Type)})
258329
}
@@ -350,13 +421,13 @@ func serializeMicroflowDataType(dt microflows.DataType) bson.D {
350421

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

357428
// Add parameters first (they appear at the top of the microflow)
358429
for i, p := range params {
359-
objects = append(objects, serializeMicroflowParameter(p, i))
430+
objects = append(objects, serializeMicroflowParameter(p, i, majorVersion))
360431
}
361432

362433
// Add regular microflow objects
@@ -404,16 +475,21 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
404475
}
405476

406477
case *microflows.EndEvent:
478+
// Pristine EndEvents always carry `ReturnValue` (empty string for void
479+
// microflows; expression + "\n" when a value is returned). Omitting it
480+
// diverges from the pristine key set on Mx 9 roundtrips.
481+
returnValue := ""
482+
if o.ReturnValue != "" {
483+
returnValue = o.ReturnValue + "\n"
484+
}
407485
doc := bson.D{
408486
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
409487
{Key: "$Type", Value: "Microflows$EndEvent"},
410488
{Key: "Documentation", Value: ""},
411489
{Key: "RelativeMiddlePoint", Value: pointToString(o.Position)},
490+
{Key: "ReturnValue", Value: returnValue},
491+
{Key: "Size", Value: sizeToString(o.Size)},
412492
}
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)})
417493
return doc
418494

419495
case *microflows.ErrorEvent:
@@ -450,6 +526,7 @@ func serializeMicroflowObject(obj microflows.MicroflowObject) bson.D {
450526
{Key: "$ID", Value: idToBsonBinary(string(o.ID))},
451527
{Key: "$Type", Value: "Microflows$ExclusiveSplit"},
452528
{Key: "Caption", Value: o.Caption},
529+
{Key: "Documentation", Value: o.Documentation},
453530
{Key: "ErrorHandlingType", Value: string(o.ErrorHandlingType)},
454531
{Key: "RelativeMiddlePoint", Value: pointToString(o.Position)},
455532
{Key: "Size", Value: sizeToString(o.Size)},

0 commit comments

Comments
 (0)