diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 35861011..13795afe 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 | | `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 | --- 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 00000000..8aca3cf4 --- /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 aa4c4580..c2cf73f2 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 99cd3419..74462f0e 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") + } +}