diff --git a/mdl-examples/bug-tests/360-import-mapping-result-type.mdl b/mdl-examples/bug-tests/360-import-mapping-result-type.mdl new file mode 100644 index 00000000..ad53d9b1 --- /dev/null +++ b/mdl-examples/bug-tests/360-import-mapping-result-type.mdl @@ -0,0 +1,66 @@ +-- ============================================================================ +-- Bug #360: Import-from-mapping result variable type was not registered +-- ============================================================================ +-- +-- Symptom (before fix): +-- `import from mapping` actions wrote the correct +-- `Forms$ResultHandlingMapping` BSON, but the builder did not register +-- the inferred result type onto the output variable in its variable +-- scope. Subsequent activities that touched the imported result +-- (`change`, `commit`, `$Var/Attribute`, association paths) ran without +-- type info, which caused: +-- - the writer to fall back to untyped expressions +-- - re-describes to lose the right declaration shape +-- - downstream activities to fail with CE0117 in Studio Pro +-- +-- After fix: +-- `addImportFromMappingAction` now also writes the result type into +-- `flowBuilder.varTypes`. When the mapping's JSON structure produces a +-- single object, the variable becomes `Module.Entity`. When it produces +-- an array, the variable becomes `List of Module.Entity`. The +-- ResultHandlingMapping BSON path is unchanged. +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/360-import-mapping-result-type.mdl -p app.mpr +-- mxcli -p app.mpr -c "describe microflow BugTest360.MF_ImportPet" +-- `mx check` against the resulting MPR must report 0 errors and the +-- `change $Pet (...)` statement after the import must resolve. +-- ============================================================================ + +create module BugTest360; + +create json structure BugTest360.JSON_Pet +snippet '{"id": 1, "name": "Fido", "status": "available"}'; + +create non-persistent entity BugTest360.PetResponse ( + PetId : integer, + Name : string, + Status : string +); +/ + +create import mapping BugTest360.IMM_Pet + with json structure BugTest360.JSON_Pet +{ + create BugTest360.PetResponse { + PetId = id, + Name = name, + Status = status + } +}; + +-- Caller — uses the imported `$Pet` in a downstream `change`. Without +-- the result-type registration, the change activity has no resolved +-- entity for `Status` and Studio Pro surfaces CE0117. +create microflow BugTest360.MF_ImportPet ( + $Json: string +) +returns BugTest360.PetResponse as $Pet +begin + $Pet = import from mapping BugTest360.IMM_Pet ($Json); + + if $Pet != empty then + change $Pet (Status = $Pet/Status + ' (processed)'); + end if; +end; +/ diff --git a/mdl/executor/cmd_microflows_builder_calls.go b/mdl/executor/cmd_microflows_builder_calls.go index a19f9a33..362254e3 100644 --- a/mdl/executor/cmd_microflows_builder_calls.go +++ b/mdl/executor/cmd_microflows_builder_calls.go @@ -1012,7 +1012,8 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) SingleObject: true, } - // Determine single vs list and result entity from the import mapping + // Determine single vs list and result entity from the import mapping. + resultEntityQN := "" if fb.backend != nil { if im, err := fb.backend.GetImportMappingByQualifiedName(s.Mapping.Module, s.Mapping.Name); err == nil { if im.JsonStructure != "" { @@ -1026,7 +1027,8 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) } } if len(im.Elements) > 0 && im.Elements[0].Entity != "" { - resultHandling.ResultEntityID = model.ID(im.Elements[0].Entity) + resultEntityQN = im.Elements[0].Entity + resultHandling.ResultEntityID = model.ID(resultEntityQN) } } } @@ -1047,6 +1049,13 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) fb.objects = append(fb.objects, activity) fb.posX += fb.spacing + if s.OutputVariable != "" && resultEntityQN != "" && fb.varTypes != nil { + if resultHandling.SingleObject { + fb.varTypes[s.OutputVariable] = resultEntityQN + } else { + fb.varTypes[s.OutputVariable] = "List of " + resultEntityQN + } + } if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 { errorY := fb.posY + VerticalSpacing diff --git a/mdl/executor/cmd_microflows_builder_import_mapping_test.go b/mdl/executor/cmd_microflows_builder_import_mapping_test.go new file mode 100644 index 00000000..dc1a957a --- /dev/null +++ b/mdl/executor/cmd_microflows_builder_import_mapping_test.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + mdltypes "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" +) + +func TestAddImportFromMappingRegistersSingleResultType(t *testing.T) { + fb := importMappingFlowBuilder(t, "Object") + + fb.addImportFromMappingAction(&ast.ImportFromMappingStmt{ + OutputVariable: "ImportedOrder", + SourceVariable: "Payload", + Mapping: ast.QualifiedName{Module: "Integration", Name: "ImportOrder"}, + }) + + if got := fb.varTypes["ImportedOrder"]; got != "Sales.Order" { + t.Fatalf("ImportedOrder type = %q, want Sales.Order", got) + } +} + +func TestAddImportFromMappingRegistersListResultType(t *testing.T) { + fb := importMappingFlowBuilder(t, "Array") + + fb.addImportFromMappingAction(&ast.ImportFromMappingStmt{ + OutputVariable: "ImportedOrders", + SourceVariable: "Payload", + Mapping: ast.QualifiedName{Module: "Integration", Name: "ImportOrderList"}, + }) + + if got := fb.varTypes["ImportedOrders"]; got != "List of Sales.Order" { + t.Fatalf("ImportedOrders type = %q, want list of Sales.Order", got) + } +} + +func importMappingFlowBuilder(t *testing.T, rootElementType string) *flowBuilder { + t.Helper() + + return &flowBuilder{ + varTypes: map[string]string{}, + backend: &mock.MockBackend{ + GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) { + if moduleName != "Integration" { + return nil, fmt.Errorf("unexpected module %q", moduleName) + } + return &model.ImportMapping{ + JsonStructure: "Integration.OrderPayload", + Elements: []*model.ImportMappingElement{ + {Entity: "Sales.Order"}, + }, + }, nil + }, + GetJsonStructureByQualifiedNameFunc: func(moduleName, name string) (*mdltypes.JsonStructure, error) { + if moduleName != "Integration" || name != "OrderPayload" { + return nil, fmt.Errorf("unexpected json structure %s.%s", moduleName, name) + } + return &mdltypes.JsonStructure{ + Elements: []*mdltypes.JsonElement{ + {ElementType: rootElementType}, + }, + }, nil + }, + }, + } +}