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
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| CE0463 "widget definition changed" on every pluggable widget that contains a caption/template parameter (gallery, datagrid2 captions, dynamictext with ContentParams) on Mendix 11.9 — cascades into CE3637 on master-detail pages | `serializeClientTemplateParameter` emitted `Forms$FormattingInfo` with a `TimeFormat: "HoursMinutes"` field, but the FormattingInfo reflection schema only declares CustomDateFormat / DateFormat / DecimalPrecision / EnumFormat / GroupDigits — the extra field made Studio Pro mark the embedded WidgetType as drifted | `sdk/mpr/writer_widgets.go` `serializeClientTemplateParameter` + `mdl/backend/mpr/widget_builder.go` (mirror copy of the same FormattingInfo block) | Drop the `TimeFormat` entry from both writers. Verify by diffing your BSON against a Studio Pro-saved page's FormattingInfo block — if you see keys outside the reflection schema, that's the trigger |
| 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 |

---
Expand Down
38 changes: 38 additions & 0 deletions mdl-examples/bug-tests/573-odata-microflow-prefix.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- ============================================================================
-- Bug #573 — CREATE ODATA CLIENT stored ConfigurationMicroflow and
-- ErrorHandlingMicroflow with a literal "MICROFLOW " prefix in their BSON
-- values. Mendix then tried to resolve a microflow whose qualified name was
-- literally "MICROFLOW MyModule.HandleError" and failed.
--
-- Root cause: the visitor emitted uppercase "MICROFLOW Module.Name" for
-- `microflow ...` property values, but extractMicroflowRef in cmd_odata.go
-- only stripped lowercase "microflow ". Fixed by making the strip
-- case-insensitive.
--
-- This script should round-trip cleanly through `mxcli exec` and
-- Studio Pro's mx-check.
-- ============================================================================

create module Issue573;

create microflow Issue573.ConfigureRequest () returns void
begin
end;

create microflow Issue573.HandleError () returns void
begin
end;

create odata client Issue573.MyService (
ODataVersion: OData4,
MetadataUrl: 'https://example.com/odata/$metadata',
Timeout: 300,
ConfigurationMicroflow: microflow Issue573.ConfigureRequest,
ErrorHandlingMicroflow: microflow Issue573.HandleError
);

-- Verify: the describe output below must contain
-- ConfigurationMicroflow: microflow Issue573.ConfigureRequest
-- and NOT
-- ConfigurationMicroflow: microflow MICROFLOW Issue573.ConfigureRequest
describe odata client Issue573.MyService;
12 changes: 9 additions & 3 deletions mdl/executor/cmd_odata.go
Original file line number Diff line number Diff line change
Expand Up @@ -1485,10 +1485,16 @@ func formatExprValue(val string) string {
return "'" + strings.ReplaceAll(val, "'", "''") + "'"
}

// extractMicroflowRef strips "MICROFLOW " prefix from a microflow reference string.
// Both "MICROFLOW Module.Name" and "Module.Name" formats are accepted.
// extractMicroflowRef strips a leading "microflow " keyword (any case) from a
// microflow reference string. The visitor emits uppercase `"MICROFLOW " + qn`
// for `microflow Module.Name` property values (see visitor_odata.go); both
// that form and a bare `Module.Name` are accepted. Issue #573.
func extractMicroflowRef(ref string) string {
return strings.TrimPrefix(ref, "microflow ")
const prefix = "microflow "
if len(ref) >= len(prefix) && strings.EqualFold(ref[:len(prefix)], prefix) {
return ref[len(prefix):]
}
return ref
}

// astEntityDefToModel converts an AST PublishedEntityDef to model PublishedEntityType
Expand Down
105 changes: 105 additions & 0 deletions mdl/executor/cmd_odata_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,108 @@ func TestValidateMetadataURL_RejectsBarWords(t *testing.T) {
}
}
}

// TestCreateODataClient_StripsMicroflowPrefix_Issue573 verifies that the
// "microflow " keyword prefix the visitor emits in front of a qualified name
// is stripped before the value reaches BSON.
//
// The visitor at mdl/visitor/visitor_odata.go emits uppercase "MICROFLOW "
// for `microflow Module.Name` property values. extractMicroflowRef used to
// strip only lowercase "microflow ", so the prefix survived all the way to
// BSON and Mendix tried to resolve a microflow whose qualified name was
// literally "MICROFLOW Module.Name" — see issue #573.
func TestCreateODataClient_StripsMicroflowPrefix_Issue573(t *testing.T) {
mod := mkModule("MyModule")
h := mkHierarchy(mod)

var captured *model.ConsumedODataService
mb := &mock.MockBackend{
IsConnectedFunc: func() bool { return true },
ListModulesFunc: func() ([]*model.Module, error) {
return []*model.Module{mod}, nil
},
ListConsumedODataServicesFunc: func() ([]*model.ConsumedODataService, error) {
return nil, nil
},
CreateConsumedODataServiceFunc: func(svc *model.ConsumedODataService) error {
captured = svc
return nil
},
}
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))

stmt := &ast.CreateODataClientStmt{
Name: ast.QualifiedName{Module: "MyModule", Name: "MyService"},
ODataVersion: "OData4",
MetadataUrl: "https://example.com/odata/$metadata",
ConfigurationMicroflow: "MICROFLOW MyModule.ConfigureRequest",
ErrorHandlingMicroflow: "MICROFLOW MyModule.HandleError",
}
assertNoError(t, createODataClient(ctx, stmt))

if captured == nil {
t.Fatal("CreateConsumedODataService was not called")
}
if captured.ConfigurationMicroflow != "MyModule.ConfigureRequest" {
t.Errorf("ConfigurationMicroflow = %q, want %q (uppercase \"MICROFLOW \" prefix not stripped)",
captured.ConfigurationMicroflow, "MyModule.ConfigureRequest")
}
if captured.ErrorHandlingMicroflow != "MyModule.HandleError" {
t.Errorf("ErrorHandlingMicroflow = %q, want %q (uppercase \"MICROFLOW \" prefix not stripped)",
captured.ErrorHandlingMicroflow, "MyModule.HandleError")
}
}

// TestCreateODataClient_VisitorRoundtrip_Issue573 is the user-facing scenario
// for issue #573: parse the exact MDL the user wrote, run the executor, and
// confirm the value handed to the backend is the bare qualified name.
func TestCreateODataClient_VisitorRoundtrip_Issue573(t *testing.T) {
mod := mkModule("MyModule")
h := mkHierarchy(mod)

var captured *model.ConsumedODataService
mb := &mock.MockBackend{
IsConnectedFunc: func() bool { return true },
ListModulesFunc: func() ([]*model.Module, error) {
return []*model.Module{mod}, nil
},
ListConsumedODataServicesFunc: func() ([]*model.ConsumedODataService, error) {
return nil, nil
},
CreateConsumedODataServiceFunc: func(svc *model.ConsumedODataService) error {
captured = svc
return nil
},
}
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))

const script = `CREATE ODATA CLIENT MyModule.MyService (
ODataVersion: OData4,
MetadataUrl: 'https://example.com/odata/$metadata',
Timeout: 300,
ConfigurationMicroflow: microflow MyModule.ConfigureRequest,
ErrorHandlingMicroflow: microflow MyModule.HandleError
);`
prog, errs := visitor.Build(script)
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
if len(prog.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(prog.Statements))
}
stmt, ok := prog.Statements[0].(*ast.CreateODataClientStmt)
if !ok {
t.Fatalf("expected *CreateODataClientStmt, got %T", prog.Statements[0])
}
assertNoError(t, createODataClient(ctx, stmt))

if captured == nil {
t.Fatal("CreateConsumedODataService was not called")
}
if captured.ConfigurationMicroflow != "MyModule.ConfigureRequest" {
t.Errorf("ConfigurationMicroflow = %q, want %q", captured.ConfigurationMicroflow, "MyModule.ConfigureRequest")
}
if captured.ErrorHandlingMicroflow != "MyModule.HandleError" {
t.Errorf("ErrorHandlingMicroflow = %q, want %q", captured.ErrorHandlingMicroflow, "MyModule.HandleError")
}
}
Loading