Skip to content

Commit 7b297fb

Browse files
authored
Merge pull request #261 from hjotha/submit/microflow-mx9-version-gating
fix: gate Mx 9 microflow roundtrip keys by project version
2 parents cf6860c + 971dc43 commit 7b297fb

3 files changed

Lines changed: 323 additions & 79 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- ============================================================================
2+
-- Bug #261: Mx 9 microflow roundtrip keys gated by project version
3+
-- ============================================================================
4+
--
5+
-- Symptom (before fix):
6+
-- Writing a microflow to a Mendix 9 project unconditionally emitted
7+
-- ReturnVariableName / StableId / Url / UrlSearchParameters on the
8+
-- Microflow document and DefaultValue / IsRequired on its parameters.
9+
-- Studio Pro on Mx 9 treats those as unknown metamodel keys and raises
10+
-- schema validation errors on open.
11+
--
12+
-- Similarly, SequenceFlow and AnnotationFlow were always emitted with
13+
-- the Mx 10+ shape (CaseValues array, Line document), so a Mx 9 project
14+
-- re-parsed them as malformed and dropped case values / bezier vectors.
15+
--
16+
-- After fix:
17+
-- The writer reads the project's major version from the reader and emits
18+
-- the matching key set:
19+
-- - Mx 9: legacy NewCaseValue + top-level {Origin,Destination}BezierVector
20+
-- - Mx 10+: modern CaseValues + Line: Microflows$BezierCurve
21+
--
22+
-- Usage:
23+
-- mxcli exec mdl-examples/bug-tests/261-mx9-microflow-roundtrip.mdl -p mx9-app.mpr
24+
-- Open in Studio Pro 9 — project must load without metamodel errors.
25+
-- ============================================================================
26+
27+
create module BugTest261;
28+
29+
create microflow BugTest261.MF_Demo (
30+
$name: string
31+
)
32+
returns string as $result
33+
begin
34+
declare $result string = empty;
35+
set $result = $name;
36+
return $result;
37+
end;
38+
/

sdk/mpr/writer_microflow.go

Lines changed: 147 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
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

Comments
 (0)