From b4d2d1756d937a7097f62733f195b0ab8eb5d314 Mon Sep 17 00:00:00 2001 From: Andrej Koelewijn Date: Fri, 22 May 2026 20:26:49 +0000 Subject: [PATCH] fix: CREATE ODATA CLIENT strips uppercase MICROFLOW keyword prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #573. Visitor emits "MICROFLOW Module.Name" (uppercase) for `microflow X.Y` property values, but extractMicroflowRef only stripped lowercase "microflow ", so the prefix survived into BSON. Mendix then tried to resolve a microflow whose qualified name was literally "MICROFLOW Module.Name" and failed — matching the user-reported error. Fix is a case-insensitive prefix strip in extractMicroflowRef. Same code path serves CREATE, CREATE OR MODIFY, and ALTER ODATA CLIENT. Tests: - TestCreateODataClient_StripsMicroflowPrefix_Issue573 — direct executor test against the proximate failure point. - TestCreateODataClient_VisitorRoundtrip_Issue573 — full MDL → visitor → executor pipeline with the user's exact syntax. - mdl-examples/bug-tests/573-odata-microflow-prefix.mdl — reproduction script for Studio Pro validation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/fix-issue.md | 1 + .../bug-tests/573-odata-microflow-prefix.mdl | 38 +++++++ mdl/executor/cmd_odata.go | 12 +- mdl/executor/cmd_odata_mock_test.go | 105 ++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 mdl-examples/bug-tests/573-odata-microflow-prefix.mdl diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 11b71601e..3f72b9bb0 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -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()` 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 | --- diff --git a/mdl-examples/bug-tests/573-odata-microflow-prefix.mdl b/mdl-examples/bug-tests/573-odata-microflow-prefix.mdl new file mode 100644 index 000000000..8aca3cf4b --- /dev/null +++ b/mdl-examples/bug-tests/573-odata-microflow-prefix.mdl @@ -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; diff --git a/mdl/executor/cmd_odata.go b/mdl/executor/cmd_odata.go index aa4c4580d..c2cf73f25 100644 --- a/mdl/executor/cmd_odata.go +++ b/mdl/executor/cmd_odata.go @@ -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 diff --git a/mdl/executor/cmd_odata_mock_test.go b/mdl/executor/cmd_odata_mock_test.go index 99cd3419f..74462f0e6 100644 --- a/mdl/executor/cmd_odata_mock_test.go +++ b/mdl/executor/cmd_odata_mock_test.go @@ -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") + } +}