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
2 changes: 1 addition & 1 deletion .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| Pluggable widget `Selection` BSON value is lowercase (`single` / `multi` / `none`) but Studio Pro stores PascalCase — contributes to CE0463 drift on stricter widgets and looks wrong in diffs | MDL passes the user's typed value verbatim to `SetSelection`; the builder didn't normalise | `mdl/backend/mpr/widget_builder.go` `SetSelection` | Canonicalise via `canonicalSelectionValue` helper (lowercase-keyed switch → `Single` / `Multi` / `None`); unknown values pass through |
| Nightly `mx check` reports `CE0117 "Error(s) in expression." at Log message activity 'Log message (warning)'` on Mendix 10.24.19+ but not 10.24.16 or 11.x | Mendix 10.24.19 tightened expression validation: `toString(<string>)` is now a type error (toString expects a non-string input). An example called `toString($OrderNumber)` where `$OrderNumber` was already a string parameter | The offending `log warning ... with ({1} = toString($stringVar))` — find via `~/.mxcli/mxbuild/{ver}/modeler/mx check`, then bisect with `drop microflow ...` until CE0117 disappears | Remove the redundant `toString()` wrapper around already-string values. Only wrap non-string values (integers, decimals, dates, enums) in `toString()`. The Mendix 11.x parser is more lenient and lets this slide, but 10.24.19+ rejects it |
| Mendix can't resolve the microflow named in `CREATE ODATA CLIENT (ConfigurationMicroflow: microflow X.Y)` / `ErrorHandlingMicroflow:` — error names the literal string `"MICROFLOW X.Y"` as the missing microflow | Case-mismatched prefix strip: visitor emits uppercase `"MICROFLOW "` from `odataValueText`, but `extractMicroflowRef` only trimmed lowercase `"microflow "`, so the keyword survived into BSON | `mdl/executor/cmd_odata.go` → `extractMicroflowRef` | Use a case-insensitive strip: `if strings.EqualFold(ref[:10], "microflow ") { return ref[10:] }`. Whenever a value goes from a visitor that emits a keyword-prefixed form to an executor that strips it, the strip must match the case the visitor produces — grep visitor files for `"MICROFLOW " +`/`"ENTITY " +`/etc. when adding a new property. Issue #573 |
| `describe entity` shows every String attribute as `String(unlimited)` and `CATALOG.ATTRIBUTES.Length` reports `0` for all of them, even though Studio Pro shows a finite max length and runtime rejects overlong values | BSON numeric width mismatch — Mendix Studio Pro stores `Length` as int64, but `parseAttributeType` used `raw["Length"].(int32)` so the assertion always failed and Length defaulted to 0 | `sdk/mpr/parser_domainmodel.go` → `parseAttributeType` `case "DomainModels$StringAttributeType"` | Replace narrow type assertions on BSON numeric fields with the existing `extractInt(raw["X"])` helper (`sdk/mpr/parser.go`), which handles int32/int64/int/float64. Sweep other `parser_*.go` files for the same `.(int32)` / `.(int64)` pattern on size/length/count fields. Issue #583 |
| `describe`/catalog reports a numeric BSON field (Length, MinOccurs, MaxOccurs, MaxLength, FractionDigits, TotalDigits, Interval, NumberOfPagesToClose, …) as `0` / `unlimited` even though Studio Pro shows a real value | BSON numeric width mismatch — Studio Pro writes the field as `int64`, but the parser asserted `raw["X"].(int32)` so the type assertion failed silently and the field defaulted to its zero value | `sdk/mpr/parser_*.go` — grep for the field name; the fix point is the narrow assertion | Replace narrow type assertions on BSON numeric fields with the existing `extractInt(raw["X"])` helper (`sdk/mpr/parser.go`). It handles int32/int64/int/float64. When a non-zero default must survive a missing field, gate with `if _, ok := raw["X"]; ok { … = extractInt(...) }`. Sweep `grep -n '\.(int32)' sdk/mpr/parser_*.go` and ignore matches whose comment says "marker" (BSON-array-prefix probes are intentional). Issues #583, #585 |

---

Expand Down
6 changes: 4 additions & 2 deletions sdk/mpr/parser_enumeration.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,10 @@ func (r *Reader) parseScheduledEvent(unitID, containerID string, contents []byte
if enabled, ok := raw["Enabled"].(bool); ok {
event.Enabled = enabled
}
if interval, ok := raw["Interval"].(int32); ok {
event.Interval = int(interval)
// Issue #585: Studio Pro stores Interval as BSON int64; extractInt
// also accepts int32/int/float64 emitted by other writers.
if _, ok := raw["Interval"]; ok {
event.Interval = extractInt(raw["Interval"])
}
if intervalType, ok := raw["IntervalType"].(string); ok {
event.IntervalType = intervalType
Expand Down
8 changes: 4 additions & 4 deletions sdk/mpr/parser_microflow_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,10 @@ func parseShowHomePageAction(raw map[string]any) *microflows.ShowHomePageAction
func parseClosePageAction(raw map[string]any) *microflows.ClosePageAction {
action := &microflows.ClosePageAction{}
action.ID = model.ID(extractBsonID(raw["$ID"]))
if numPages, ok := raw["NumberOfPagesToClose"].(int32); ok {
action.NumberOfPages = int(numPages)
} else if numPages, ok := raw["NumberOfPagesToClose"].(int64); ok {
action.NumberOfPages = int(numPages)
// Issue #585: collapse the int32/int64 dispatch to the shared extractInt
// helper. Default of 1 is preserved when the field is absent.
if _, ok := raw["NumberOfPagesToClose"]; ok {
action.NumberOfPages = extractInt(raw["NumberOfPagesToClose"])
} else {
action.NumberOfPages = 1
}
Expand Down
24 changes: 14 additions & 10 deletions sdk/mpr/parser_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -759,26 +759,30 @@ func parseJsonElement(raw map[string]any) *JsonElement {
if v, ok := raw["PrimitiveType"].(string); ok {
elem.PrimitiveType = v
}
if v, ok := raw["MinOccurs"].(int32); ok {
elem.MinOccurs = int(v)
// Issue #585: Studio Pro writes these numeric facets as BSON int64;
// mxcli's writer emits int32. extractInt accepts both (plus int and
// float64). Default values for MaxLength/FractionDigits/TotalDigits
// stay at -1 (set in the literal above) when the field is absent.
if _, ok := raw["MinOccurs"]; ok {
elem.MinOccurs = extractInt(raw["MinOccurs"])
}
if v, ok := raw["MaxOccurs"].(int32); ok {
elem.MaxOccurs = int(v)
if _, ok := raw["MaxOccurs"]; ok {
elem.MaxOccurs = extractInt(raw["MaxOccurs"])
}
if v, ok := raw["Nillable"].(bool); ok {
elem.Nillable = v
}
if v, ok := raw["IsDefaultType"].(bool); ok {
elem.IsDefaultType = v
}
if v, ok := raw["MaxLength"].(int32); ok {
elem.MaxLength = int(v)
if _, ok := raw["MaxLength"]; ok {
elem.MaxLength = extractInt(raw["MaxLength"])
}
if v, ok := raw["FractionDigits"].(int32); ok {
elem.FractionDigits = int(v)
if _, ok := raw["FractionDigits"]; ok {
elem.FractionDigits = extractInt(raw["FractionDigits"])
}
if v, ok := raw["TotalDigits"].(int32); ok {
elem.TotalDigits = int(v)
if _, ok := raw["TotalDigits"]; ok {
elem.TotalDigits = extractInt(raw["TotalDigits"])
}
if v, ok := raw["OriginalValue"].(string); ok {
elem.OriginalValue = v
Expand Down
59 changes: 59 additions & 0 deletions sdk/mpr/parser_misc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0

package mpr

import (
"testing"
)

// Issue #585: parseJsonElement asserted `raw[field].(int32)` for every numeric
// facet on a JSON-structure element. Mendix Studio Pro stores these fields as
// BSON int64, so the assertion failed silently and the parsed value defaulted
// to 0 — the same class of bug fixed for StringAttributeType.Length in #583.
//
// Each numeric facet must round-trip across every BSON numeric width that the
// mongo-driver may produce (int32, int64, int, float64) and preserve the
// default when the field is missing.
func TestParseJsonElement_NumericFields_BsonNumericWidths(t *testing.T) {
type fieldCase struct {
name string
bsonKey string
read func(*JsonElement) int
missing int // expected zero value when field is absent
}
fields := []fieldCase{
{"MinOccurs", "MinOccurs", func(e *JsonElement) int { return e.MinOccurs }, 0},
{"MaxOccurs", "MaxOccurs", func(e *JsonElement) int { return e.MaxOccurs }, 0},
{"MaxLength", "MaxLength", func(e *JsonElement) int { return e.MaxLength }, -1},
{"FractionDigits", "FractionDigits", func(e *JsonElement) int { return e.FractionDigits }, -1},
{"TotalDigits", "TotalDigits", func(e *JsonElement) int { return e.TotalDigits }, -1},
}

widths := []struct {
name string
value any
}{
{"int32 (mxcli writer)", int32(42)},
{"int64 (Studio Pro writer)", int64(42)},
{"int", int(42)},
{"float64 (extended JSON)", float64(42)},
}

for _, f := range fields {
for _, w := range widths {
t.Run(f.name+"/"+w.name, func(t *testing.T) {
raw := map[string]any{f.bsonKey: w.value}
elem := parseJsonElement(raw)
if got := f.read(elem); got != 42 {
t.Errorf("%s = %d, want 42 (input %T(%v))", f.name, got, w.value, w.value)
}
})
}
t.Run(f.name+"/missing", func(t *testing.T) {
elem := parseJsonElement(map[string]any{})
if got := f.read(elem); got != f.missing {
t.Errorf("%s = %d, want default %d when field absent", f.name, got, f.missing)
}
})
}
}
54 changes: 54 additions & 0 deletions sdk/mpr/parser_scheduledevent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0

package mpr

import (
"testing"

"go.mongodb.org/mongo-driver/bson"
)

// Issue #585: parseScheduledEvent asserted `raw["Interval"].(int32)`. Studio
// Pro writes Interval as BSON int64, so the assertion failed silently and
// every scheduled event read from a Studio Pro-written MPR appeared with
// Interval=0 — the same misreport pattern fixed in #583 for
// StringAttributeType.Length.
func TestParseScheduledEvent_Interval_BsonNumericWidths(t *testing.T) {
cases := []struct {
name string
interval any
want int
}{
{"int32 (mxcli writer)", int32(15), 15},
{"int64 (Studio Pro writer)", int64(15), 15},
{"int", int(15), 15},
{"float64 (extended JSON)", float64(15), 15},
{"missing field", nil, 0},
}

r := &Reader{version: MPRVersionV1}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
doc := bson.M{
"$Type": "ScheduledEvents$ScheduledEvent",
"Name": "MyEvent",
"Enabled": true,
"IntervalType": "Hour",
}
if tc.interval != nil {
doc["Interval"] = tc.interval
}
data, err := bson.Marshal(doc)
if err != nil {
t.Fatalf("bson.Marshal: %v", err)
}
event, err := r.parseScheduledEvent("unit-id", "container-id", data)
if err != nil {
t.Fatalf("parseScheduledEvent: %v", err)
}
if event.Interval != tc.want {
t.Errorf("Interval = %d, want %d (input %T(%v))", event.Interval, tc.want, tc.interval, tc.interval)
}
})
}
}
Loading