Skip to content

Commit 33f385e

Browse files
committed
fixup: address PR #261 review feedback
- Replace hardcoded `majorVersion := 11` with `version.DefaultVersion().MajorVersion` so the fallback stays in sync with the project default. - Normalise value-receiver case values in `buildSequenceFlowCase` so EnumerationCase/NoCase are each handled once (pointer promotion before the main switch). - Add `writer_microflow_version_test.go` regression tests exercising Mx 9 vs Mx 10 serialization shapes for SequenceFlow, AnnotationFlow and MicroflowParameter. - Add `mdl-examples/bug-tests/261-mx9-microflow-roundtrip.mdl` reproducer per CLAUDE.md checklist.
1 parent 457b70e commit 33f385e

3 files changed

Lines changed: 184 additions & 20 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
customer: BugTest261.Customer
31+
)
32+
returns string
33+
(
34+
set $return = 'ok'
35+
);

sdk/mpr/writer_microflow.go

Lines changed: 11 additions & 20 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
)
@@ -106,7 +107,8 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
106107

107108
// Add Flows array (SequenceFlows and AnnotationFlows go here, not in ObjectCollection)
108109
// 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+
// Fall back to the project default when no MPR is attached (in-memory tests).
111+
majorVersion := version.DefaultVersion().MajorVersion
110112
if pv := w.reader.ProjectVersion(); pv != nil {
111113
majorVersion = pv.MajorVersion
112114
}
@@ -220,18 +222,16 @@ func serializeSequenceFlow(flow *microflows.SequenceFlow, majorVersion int) bson
220222
// When no case has been set on the flow, a NoCase document is synthesised —
221223
// Studio Pro requires every SequenceFlow to carry an explicit case object.
222224
func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
225+
// Normalise value receivers to pointers so each case is handled once.
223226
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-
}
234227
case microflows.EnumerationCase:
228+
cv = &c
229+
case microflows.NoCase:
230+
cv = &c
231+
}
232+
233+
switch c := cv.(type) {
234+
case *microflows.EnumerationCase:
235235
id := string(c.ID)
236236
if id == "" {
237237
id = generateUUID()
@@ -250,15 +250,6 @@ func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
250250
{Key: "$ID", Value: idToBsonBinary(id)},
251251
{Key: "$Type", Value: "Microflows$NoCase"},
252252
}
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-
}
262253
}
263254
// Default: synthesise a NoCase document with a fresh ID.
264255
return bson.D{
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package mpr
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/model"
9+
"github.com/mendixlabs/mxcli/sdk/microflows"
10+
11+
"go.mongodb.org/mongo-driver/bson"
12+
)
13+
14+
// bsonHasKey returns true when the top-level BSON document contains the key.
15+
func bsonHasKey(doc bson.D, key string) bool {
16+
for _, e := range doc {
17+
if e.Key == key {
18+
return true
19+
}
20+
}
21+
return false
22+
}
23+
24+
// bsonGetKey returns the value of a key or nil if absent.
25+
func bsonGetKey(doc bson.D, key string) any {
26+
for _, e := range doc {
27+
if e.Key == key {
28+
return e.Value
29+
}
30+
}
31+
return nil
32+
}
33+
34+
func TestSerializeSequenceFlow_Mx9_UsesLegacyShape(t *testing.T) {
35+
flow := &microflows.SequenceFlow{
36+
BaseElement: model.BaseElement{ID: "flow-1"},
37+
OriginID: "orig-1",
38+
DestinationID: "dest-1",
39+
CaseValue: &microflows.NoCase{BaseElement: model.BaseElement{ID: "case-1"}},
40+
}
41+
42+
doc := serializeSequenceFlow(flow, 9)
43+
44+
if !bsonHasKey(doc, "NewCaseValue") {
45+
t.Error("Mx 9 sequence flow must include NewCaseValue")
46+
}
47+
if bsonHasKey(doc, "CaseValues") {
48+
t.Error("Mx 9 sequence flow must NOT include CaseValues")
49+
}
50+
if !bsonHasKey(doc, "OriginBezierVector") || !bsonHasKey(doc, "DestinationBezierVector") {
51+
t.Error("Mx 9 sequence flow must include top-level {Origin,Destination}BezierVector")
52+
}
53+
if bsonHasKey(doc, "Line") {
54+
t.Error("Mx 9 sequence flow must NOT nest vectors under Line")
55+
}
56+
}
57+
58+
func TestSerializeSequenceFlow_Mx10_UsesModernShape(t *testing.T) {
59+
flow := &microflows.SequenceFlow{
60+
BaseElement: model.BaseElement{ID: "flow-1"},
61+
OriginID: "orig-1",
62+
DestinationID: "dest-1",
63+
CaseValue: &microflows.NoCase{BaseElement: model.BaseElement{ID: "case-1"}},
64+
}
65+
66+
doc := serializeSequenceFlow(flow, 10)
67+
68+
if !bsonHasKey(doc, "CaseValues") {
69+
t.Error("Mx 10 sequence flow must include CaseValues")
70+
}
71+
if bsonHasKey(doc, "NewCaseValue") {
72+
t.Error("Mx 10 sequence flow must NOT include legacy NewCaseValue")
73+
}
74+
if !bsonHasKey(doc, "Line") {
75+
t.Error("Mx 10 sequence flow must nest vectors under Line")
76+
}
77+
if bsonHasKey(doc, "OriginBezierVector") || bsonHasKey(doc, "DestinationBezierVector") {
78+
t.Error("Mx 10 sequence flow must NOT include top-level BezierVector fields")
79+
}
80+
}
81+
82+
func TestSerializeAnnotationFlow_VersionShapes(t *testing.T) {
83+
af := &microflows.AnnotationFlow{
84+
BaseElement: model.BaseElement{ID: "af-1"},
85+
OriginID: "orig-1",
86+
DestinationID: "dest-1",
87+
}
88+
89+
mx9 := serializeAnnotationFlow(af, 9)
90+
if !bsonHasKey(mx9, "OriginBezierVector") || !bsonHasKey(mx9, "DestinationBezierVector") {
91+
t.Error("Mx 9 annotation flow must use top-level BezierVector fields")
92+
}
93+
if bsonHasKey(mx9, "Line") {
94+
t.Error("Mx 9 annotation flow must NOT nest under Line")
95+
}
96+
97+
mx10 := serializeAnnotationFlow(af, 10)
98+
if !bsonHasKey(mx10, "Line") {
99+
t.Error("Mx 10 annotation flow must nest vectors under Line")
100+
}
101+
if bsonHasKey(mx10, "OriginBezierVector") {
102+
t.Error("Mx 10 annotation flow must NOT include top-level BezierVector")
103+
}
104+
}
105+
106+
func TestSerializeMicroflowParameter_Mx9_OmitsMx10OnlyKeys(t *testing.T) {
107+
p := &microflows.MicroflowParameter{
108+
BaseElement: model.BaseElement{ID: "p-1"},
109+
Name: "Customer",
110+
Type: &microflows.StringType{},
111+
}
112+
113+
mx9 := serializeMicroflowParameter(p, 0, 9)
114+
if bsonHasKey(mx9, "DefaultValue") {
115+
t.Error("Mx 9 parameter must NOT emit DefaultValue")
116+
}
117+
if bsonHasKey(mx9, "IsRequired") {
118+
t.Error("Mx 9 parameter must NOT emit IsRequired")
119+
}
120+
121+
mx10 := serializeMicroflowParameter(p, 0, 10)
122+
if !bsonHasKey(mx10, "DefaultValue") {
123+
t.Error("Mx 10 parameter must emit DefaultValue")
124+
}
125+
if !bsonHasKey(mx10, "IsRequired") {
126+
t.Error("Mx 10 parameter must emit IsRequired")
127+
}
128+
}
129+
130+
func TestBuildSequenceFlowCase_NormalisesValueReceiver(t *testing.T) {
131+
// A value-receiver NoCase must produce the same shape as a pointer.
132+
fromValue := buildSequenceFlowCase(microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}})
133+
fromPointer := buildSequenceFlowCase(&microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}})
134+
135+
if bsonGetKey(fromValue, "$Type") != bsonGetKey(fromPointer, "$Type") {
136+
t.Error("value and pointer NoCase must produce identical $Type")
137+
}
138+
}

0 commit comments

Comments
 (0)