diff --git a/.claude/skills/mendix/write-microflows.md b/.claude/skills/mendix/write-microflows.md index 348fc25f..b8d8846d 100644 --- a/.claude/skills/mendix/write-microflows.md +++ b/.claude/skills/mendix/write-microflows.md @@ -882,7 +882,10 @@ rest call delete 'https://api.example.com/items/{1}' with ( - `returns string` — response body as string variable - `returns nothing` / `returns none` — ignore response - `returns response` — returns `System.HttpResponse` object -- `returns mapping Module.ImportMapping as Module.Entity` — import mapping +- `returns mapping Module.ImportMapping as Module.Entity` — single object result +- `returns mapping Module.ImportMapping as list of Module.Entity` — list result + +**Pick `as` vs `as list of` based on the call site, not the mapping shape.** The same import mapping can yield either a single object or a list — Studio Pro stores the cardinality on the microflow's `ImportMappingCall` (`Range.SingleObject` + `ForceSingleOccurrence`). Use `as Module.Entity` when the response is a single object (the mapping may still be list-typed; Studio Pro binds the first item). Use `as list of Module.Entity` when the response should bind a list. Mismatching the cardinality with the surrounding code produces `mx check` `CE0117` at the End event or `CE0013` / `CE0100` on downstream loop / aggregate / list-operation activities. **REST CALL supports full error handling** (`on error continue`, `on error rollback`, custom error handlers). diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 3932d825..805c8522 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -231,6 +231,11 @@ authentication basic, session | Call Java action | `$Result = call java action Module.Name (Param = $value);` | Java action (microflow only) | | Call web service | `$Result = call web service Module.Service operation OperationName;` | Legacy SOAP; quoted refs are fallback for dangling raw IDs | | Call web service raw | `$Result = call web service raw 'base64-bson';` | Escape hatch for byte-for-byte legacy SOAP round-trip | +| REST call (string) | `$Var = rest call get '' returns string;` | Body as string | +| REST call (response) | `$Var = rest call get '' returns response;` | `System.HttpResponse` object | +| REST call (mapping single) | `$Var = rest call get '' returns mapping Module.IMM as Module.Entity;` | Single object — Studio Pro emits `ForceSingleOccurrence=true` | +| REST call (mapping list) | `$Var = rest call get '' returns mapping Module.IMM as list of Module.Entity;` | List result | +| REST call (none) | `rest call get '' returns nothing;` | Discard response | | Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` | | Close page | `close page;` | | | Download file | `download file $FileDocument [show in browser];` | Streams a `System.FileDocument` | diff --git a/mdl-examples/bug-tests/519-rest-mapping-as-list-of.mdl b/mdl-examples/bug-tests/519-rest-mapping-as-list-of.mdl new file mode 100644 index 00000000..56b77457 --- /dev/null +++ b/mdl-examples/bug-tests/519-rest-mapping-as-list-of.mdl @@ -0,0 +1,90 @@ +-- ============================================================================ +-- Bug #519: REST mapping result cardinality must be authored at the call site +-- ============================================================================ +-- +-- Symptom (before fix): +-- `returns mapping Module.IMM as Module.Entity` was the only form available. +-- The same import mapping can yield either a single object or a list +-- depending on the call site — Studio Pro stores the cardinality on the +-- microflow's ImportMappingCall (Range.SingleObject + ForceSingleOccurrence), +-- not on the import mapping itself. mxcli's builder had to guess from the +-- import-mapping shape, which was lossy. With message-definition / XML-schema +-- backed mappings (no JsonStructure), the guess flipped between releases — +-- one version forced single, another forced list — producing CE0117 at the +-- End event or CE0013 / CE0100 on downstream loop / aggregate / list-op +-- activities depending on which way the heuristic went. +-- +-- After fix: +-- New MDL syntax `returns mapping Module.IMM as list of Module.Entity` makes +-- the cardinality explicit at the call site. The describer emits `as list of` +-- when SingleObject=false and `as` otherwise, so the roundtrip preserves the +-- exact cardinality Studio Pro authored. The builder mirrors SingleObject +-- into ForceSingleOccurrence so the writer reproduces the BSON layout +-- Studio Pro emits. +-- +-- Validation: +-- `mxcli check 519-rest-mapping-as-list-of.mdl` parses both forms. +-- Roundtrip (describe → exec → describe) preserves both cardinality forms +-- byte-for-byte. `mx check` against the resulting MPR reports 0 errors for +-- the single-object case (consumed via `return $Item`) and the list case +-- (consumed via `count($Items)` and `loop`). +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/519-rest-mapping-as-list-of.mdl -p app.mpr +-- mxcli -p app.mpr -c "describe microflow BugTest519.MF_GetSinglePet" +-- mxcli -p app.mpr -c "describe microflow BugTest519.MF_GetPetList" +-- ============================================================================ + +create module BugTest519; + +create json structure BugTest519.JSON_Pets +snippet '[{"id": 1, "name": "Fido", "status": "available"}]'; + +create non-persistent entity BugTest519.PetResponse ( + PetId : integer, + Name : string, + Status : string +); +/ + +create import mapping BugTest519.IMM_Pet + with json structure BugTest519.JSON_Pets +{ + create BugTest519.PetResponse { + PetId = id, + Name = name, + Status = status + } +}; + +-- Single-object form: pull the first element of a list-typed mapping into a +-- single result. Mirrors the PrivateCloudData.REST_GetEnvironmentByUUID +-- pattern (ForceSingleOccurrence=true, Range.SingleObject=false). +create microflow BugTest519.MF_GetSinglePet ( + $Url : string +) +returns BugTest519.PetResponse +begin + $Pet = rest call get $Url + timeout 30 + returns mapping BugTest519.IMM_Pet as BugTest519.PetResponse + on error rollback; + return $Pet; +end; +/ + +-- List form: bind a list result. Mirrors the MendixSSO.RetrieveUserRoles +-- pattern (ForceSingleOccurrence=false, Range.SingleObject=false). +create microflow BugTest519.MF_GetPetList ( + $Url : string +) +returns Integer +begin + $Pets = rest call get $Url + timeout 30 + returns mapping BugTest519.IMM_Pet as list of BugTest519.PetResponse + on error rollback; + declare $Count Integer = count($Pets); + return $Count; +end; +/ diff --git a/mdl-examples/doctype-tests/06-rest-client-examples.mdl b/mdl-examples/doctype-tests/06-rest-client-examples.mdl index e8384f12..479c0528 100644 --- a/mdl-examples/doctype-tests/06-rest-client-examples.mdl +++ b/mdl-examples/doctype-tests/06-rest-client-examples.mdl @@ -1457,6 +1457,33 @@ begin end; / +-- ============================================================================ +-- Level 15.2: GET single object via `as Module.Entity` +-- ============================================================================ +-- +-- The `as Module.Entity` form binds a single object — Studio Pro stores +-- this as ImportMappingCall.Range.SingleObject=true and writes +-- VariableType=ObjectType. + +create microflow RestTest.ACT_RestMappingSingleObject () +returns RestTest.PetResponse +begin + $Pet = rest call get 'https://api.example.com/pets/1' + header Accept = 'application/json' + timeout 15 + returns mapping RestTest.IMM_Pet as RestTest.PetResponse + on error rollback; + + return $Pet; +end; +/ + +-- The `as list of Module.Entity` form is exercised by the dedicated +-- bug-test `mdl-examples/bug-tests/519-rest-mapping-as-list-of.mdl`, +-- which sets up a list-rooted JSON structure (the array root needed +-- for a list-typed mapping is incompatible with the single-object +-- `JSON_Pet` declared earlier in this file). + -- ############################################################################ -- PART 16: IMPORT/EXPORT MAPPING IN MICROFLOWS -- ############################################################################ diff --git a/mdl/ast/ast_microflow.go b/mdl/ast/ast_microflow.go index 30e7ce23..f278da0a 100644 --- a/mdl/ast/ast_microflow.go +++ b/mdl/ast/ast_microflow.go @@ -730,6 +730,13 @@ type RestResult struct { Type RestResultType // Result type MappingName QualifiedName // Import mapping name (for Mapping type) ResultEntity QualifiedName // Result entity type (for Mapping type) + // IsList distinguishes `as Module.Entity` (single object) from + // `as list of Module.Entity` (list). Studio Pro stores this on the + // microflow's ImportMappingCall (Range.SingleObject / + // ForceSingleOccurrence), independently of whether the underlying + // import mapping is list-typed: the same mapping can yield either a + // single object or a list depending on this flag. + IsList bool } // RestCallStmt represents: $Var = REST CALL METHOD url [HEADER ...] [AUTH ...] [BODY ...] [TIMEOUT ...] RETURNS ... diff --git a/mdl/executor/cmd_microflows_builder_calls.go b/mdl/executor/cmd_microflows_builder_calls.go index c5a7f666..ec45f4ac 100644 --- a/mdl/executor/cmd_microflows_builder_calls.go +++ b/mdl/executor/cmd_microflows_builder_calls.go @@ -1025,26 +1025,24 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { // MDL did not explicitly assign one. s.OutputVariable = s.Result.ResultEntity.Name } - // Determine whether the import mapping returns a single object or a list by - // looking at the JSON structure it references. If the root JSON element is - // an Object, the mapping produces one object; if it is an Array, a list. - singleObject := false - if fb.backend != nil { - if im, err := fb.backend.GetImportMappingByQualifiedName(s.Result.MappingName.Module, s.Result.MappingName.Name); err == nil && im.JsonStructure != "" { - // im.JsonStructure is "Module.Name" — split and look up the JSON structure. - if parts := strings.SplitN(im.JsonStructure, ".", 2); len(parts) == 2 { - if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 { - singleObject = js.Elements[0].ElementType == "Object" - } - } - } - } + // Cardinality is authored on the microflow's ImportMappingCall in + // BSON (Range.SingleObject + ForceSingleOccurrence) — the same + // import mapping can yield either single or list depending on the + // call site. The describer emits `as list of Module.Entity` for a + // list and `as Module.Entity` for a single object; the builder + // trusts that explicit choice. ForceSingleOccurrence mirrors + // SingleObject so the writer reproduces the BSON shape Studio Pro + // emits (Range and ForceSingleOccurrence agree on whether one + // value is bound). + singleObject := !s.Result.IsList + fso := singleObject resultHandling = µflows.ResultHandlingMapping{ - BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, - MappingID: model.ID(mappingQN), - ResultEntityID: model.ID(entityQN), - ResultVariable: s.OutputVariable, - SingleObject: singleObject, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, + MappingID: model.ID(mappingQN), + ResultEntityID: model.ID(entityQN), + ResultVariable: s.OutputVariable, + SingleObject: singleObject, + ForceSingleOccurrence: &fso, } case ast.RestResultNone: resultHandling = µflows.ResultHandlingNone{ @@ -1318,9 +1316,13 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) } // Determine single vs list and result entity from the import mapping. + // JSON structure check covers JSON-backed mappings; for XML schema or + // message-definition mappings JsonStructure is empty and the root + // element kind on the mapping itself indicates Array vs Object. resultEntityQN := "" if fb.backend != nil { if im, err := fb.backend.GetImportMappingByQualifiedName(s.Mapping.Module, s.Mapping.Name); err == nil { + resolved := false if im.JsonStructure != "" { parts := strings.SplitN(im.JsonStructure, ".", 2) if len(parts) == 2 { @@ -1328,10 +1330,19 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) if js.Elements[0].ElementType == "Array" { resultHandling.SingleObject = false } + resolved = true } } } - if len(im.Elements) > 0 && im.Elements[0].Entity != "" { + if !resolved && len(im.Elements) > 0 && im.Elements[0] != nil { + // MaxOccurs > 1 or unbounded (-1) signals a list even when + // the kind is Object. + root := im.Elements[0] + if root.MaxOccurs == -1 || root.MaxOccurs > 1 { + resultHandling.SingleObject = false + } + } + if len(im.Elements) > 0 && im.Elements[0] != nil && im.Elements[0].Entity != "" { resultEntityQN = im.Elements[0].Entity resultHandling.ResultEntityID = model.ID(resultEntityQN) } diff --git a/mdl/executor/cmd_microflows_builder_import_mapping_test.go b/mdl/executor/cmd_microflows_builder_import_mapping_test.go index dc1a957a..b5841db9 100644 --- a/mdl/executor/cmd_microflows_builder_import_mapping_test.go +++ b/mdl/executor/cmd_microflows_builder_import_mapping_test.go @@ -40,6 +40,65 @@ func TestAddImportFromMappingRegistersListResultType(t *testing.T) { } } +// XML-schema and message-definition mappings have no JsonStructure; +// addImportFromMappingAction must then read the single-vs-list shape +// from the import mapping's own root element. MaxOccurs > 1 or +// unbounded (-1) signals a list — Studio Pro models a repeating Object +// root that way for these mappings. +func TestAddImportFromMappingFallsBackToImportMappingRootForListResult(t *testing.T) { + fb := &flowBuilder{ + varTypes: map[string]string{}, + backend: &mock.MockBackend{ + GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) { + return &model.ImportMapping{ + JsonStructure: "", + Elements: []*model.ImportMappingElement{ + {Kind: "Object", Entity: "Sales.Order", MaxOccurs: -1}, + }, + }, nil + }, + }, + } + + fb.addImportFromMappingAction(&ast.ImportFromMappingStmt{ + OutputVariable: "ImportedOrders", + SourceVariable: "Payload", + Mapping: ast.QualifiedName{Module: "Integration", Name: "ImportOrders"}, + }) + + if got := fb.varTypes["ImportedOrders"]; got != "List of Sales.Order" { + t.Fatalf("ImportedOrders type = %q, want list of Sales.Order (Object root with MaxOccurs=-1 must yield list)", got) + } +} + +// A non-repeating Object root (MaxOccurs ≤ 1) keeps the singleton type +// when the JSON structure is absent. +func TestAddImportFromMappingFallsBackToImportMappingRootForSingleObject(t *testing.T) { + fb := &flowBuilder{ + varTypes: map[string]string{}, + backend: &mock.MockBackend{ + GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) { + return &model.ImportMapping{ + JsonStructure: "", + Elements: []*model.ImportMappingElement{ + {Kind: "Object", Entity: "Sales.Order", MaxOccurs: 1, MinOccurs: 1}, + }, + }, nil + }, + }, + } + + 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 (Object root with MaxOccurs=1 must stay singleton)", got) + } +} + func importMappingFlowBuilder(t *testing.T, rootElementType string) *flowBuilder { t.Helper() diff --git a/mdl/executor/cmd_microflows_builder_rest_response_test.go b/mdl/executor/cmd_microflows_builder_rest_response_test.go index e1d111ff..a037425c 100644 --- a/mdl/executor/cmd_microflows_builder_rest_response_test.go +++ b/mdl/executor/cmd_microflows_builder_rest_response_test.go @@ -58,6 +58,84 @@ func TestAddRestCallAction_ReturnsResponseUsesHttpResponseHandling(t *testing.T) } } +// `as Module.Entity` (no `list of`) marks the REST result as a single +// object regardless of whether the underlying import mapping is +// list-typed. Studio Pro stores this on the microflow's +// ImportMappingCall (Range.SingleObject + ForceSingleOccurrence), so the +// builder must trust the explicit MDL syntax — using mapping shape as a +// proxy collides with cases like PCD's REST_GetEnvironmentByUUID, where +// the mapping has MaxOccurs=-1 but the call site binds a single Object. +func TestAddRestCallAction_MappingAsEntityProducesSingleObject(t *testing.T) { + fb := &flowBuilder{ + posX: 100, + posY: 100, + spacing: HorizontalSpacing, + varTypes: map[string]string{}, + declaredVars: map[string]string{}, + measurer: &layoutMeasurer{}, + } + + stmt := &ast.RestCallStmt{ + OutputVariable: "Item", + Method: ast.HttpMethodGet, + URL: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "https://example.com"}, + Result: ast.RestResult{ + Type: ast.RestResultMapping, + MappingName: ast.QualifiedName{Module: "Synthetic", Name: "MsgDefMapping"}, + ResultEntity: ast.QualifiedName{Module: "Synthetic", Name: "Item"}, + IsList: false, + }, + } + fb.addRestCallAction(stmt) + + activity := fb.objects[0].(*microflows.ActionActivity) + action := activity.Action.(*microflows.RestCallAction) + mapping := action.ResultHandling.(*microflows.ResultHandlingMapping) + if !mapping.SingleObject { + t.Errorf("SingleObject = false, want true (no `list of` => single object)") + } + if mapping.ForceSingleOccurrence == nil || !*mapping.ForceSingleOccurrence { + t.Errorf("ForceSingleOccurrence = %v, want explicit true to mirror SingleObject", mapping.ForceSingleOccurrence) + } +} + +// `as list of Module.Entity` produces a list-typed result regardless of +// the import mapping's underlying shape. ForceSingleOccurrence mirrors +// SingleObject so the writer reproduces the BSON layout Studio Pro emits. +func TestAddRestCallAction_MappingAsListOfEntityProducesListResult(t *testing.T) { + fb := &flowBuilder{ + posX: 100, + posY: 100, + spacing: HorizontalSpacing, + varTypes: map[string]string{}, + declaredVars: map[string]string{}, + measurer: &layoutMeasurer{}, + } + + stmt := &ast.RestCallStmt{ + OutputVariable: "Items", + Method: ast.HttpMethodGet, + URL: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "https://example.com"}, + Result: ast.RestResult{ + Type: ast.RestResultMapping, + MappingName: ast.QualifiedName{Module: "Synthetic", Name: "RepeatingObjectMapping"}, + ResultEntity: ast.QualifiedName{Module: "Synthetic", Name: "Item"}, + IsList: true, + }, + } + fb.addRestCallAction(stmt) + + activity := fb.objects[0].(*microflows.ActionActivity) + action := activity.Action.(*microflows.RestCallAction) + mapping := action.ResultHandling.(*microflows.ResultHandlingMapping) + if mapping.SingleObject { + t.Errorf("SingleObject = true, want false (`list of` => list result)") + } + if mapping.ForceSingleOccurrence == nil || *mapping.ForceSingleOccurrence { + t.Errorf("ForceSingleOccurrence = %v, want explicit false to mirror SingleObject", mapping.ForceSingleOccurrence) + } +} + func TestAddRestCallAction_MappingResultPreservesExplicitOutputVariable(t *testing.T) { fb := &flowBuilder{ posX: 100, diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index ffd1dedc..c83095d3 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -1296,7 +1296,16 @@ func formatRestCallAction(ctx *ExecContext, a *microflows.RestCallAction) string sb.WriteString("mapping ") sb.WriteString(string(rh.MappingID)) if rh.ResultEntityID != "" { - sb.WriteString(" as ") + // `as list of Entity` when the mapping yields a list, + // otherwise `as Entity` for a single object. Studio Pro + // keeps this on the ImportMappingCall (Range.SingleObject + // + ForceSingleOccurrence); the parser collapses both into + // SingleObject, so a list is `!SingleObject`. + if rh.SingleObject { + sb.WriteString(" as ") + } else { + sb.WriteString(" as list of ") + } sb.WriteString(string(rh.ResultEntityID)) } case *microflows.ResultHandlingNone: diff --git a/mdl/executor/cmd_microflows_format_restcall_test.go b/mdl/executor/cmd_microflows_format_restcall_test.go index 3b697fd3..a772f5b2 100644 --- a/mdl/executor/cmd_microflows_format_restcall_test.go +++ b/mdl/executor/cmd_microflows_format_restcall_test.go @@ -3,6 +3,7 @@ package executor import ( + "strings" "testing" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -108,3 +109,52 @@ func TestFormatRestCallAction_WithTimeout(t *testing.T) { got := e.formatRestCallAction(action) assertContains(t, got, "timeout 30") } + +// `returns mapping ... as Module.Entity` (no LIST_OF) describes a single +// object result. SingleObject=true must produce the bare `as` form so the +// roundtrip preserves the call site's cardinality. PrivateCloudData's +// REST_GetEnvironmentByUUID (and any REST call binding the first item of a +// list-typed mapping) depends on this form: emitting `as list of` would +// make the builder produce a ListType return value and trip CE0117 at the +// microflow's End event. +func TestFormatRestCallAction_MappingSingleObject(t *testing.T) { + e := newTestExecutor() + action := µflows.RestCallAction{ + HttpConfiguration: µflows.HttpConfiguration{ + HttpMethod: microflows.HttpMethodGet, + LocationTemplate: "https://example.com", + }, + ResultHandling: µflows.ResultHandlingMapping{ + MappingID: "Synthetic.IMM_OneItem", + ResultEntityID: "Synthetic.Item", + ResultVariable: "Item", + SingleObject: true, + }, + } + got := e.formatRestCallAction(action) + assertContains(t, got, "returns mapping Synthetic.IMM_OneItem as Synthetic.Item") + if strings.Contains(got, "as list of") { + t.Fatalf("expected single-object form, got list-of form:\n%s", got) + } +} + +// `returns mapping ... as list of Module.Entity` describes a list result. +// SingleObject=false must produce the `as list of` form so the builder +// reconstructs a ListType-bound result handling on re-execution. +func TestFormatRestCallAction_MappingListOf(t *testing.T) { + e := newTestExecutor() + action := µflows.RestCallAction{ + HttpConfiguration: µflows.HttpConfiguration{ + HttpMethod: microflows.HttpMethodGet, + LocationTemplate: "https://example.com", + }, + ResultHandling: µflows.ResultHandlingMapping{ + MappingID: "Synthetic.IMM_ManyItems", + ResultEntityID: "Synthetic.Item", + ResultVariable: "Items", + SingleObject: false, + }, + } + got := e.formatRestCallAction(action) + assertContains(t, got, "returns mapping Synthetic.IMM_ManyItems as list of Synthetic.Item") +} diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index 5594671b..6e847387 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -520,11 +520,12 @@ restCallTimeoutClause // RETURNS clause specifies how to handle the response restCallReturnsClause - : RETURNS STRING_TYPE // Return as string - | RETURNS RESPONSE // Return HttpResponse object - | RETURNS MAPPING qualifiedName AS qualifiedName // Import mapping with result entity - | RETURNS NONE // Ignore response - | RETURNS NOTHING // Ignore response (alias) + : RETURNS STRING_TYPE // Return as string + | RETURNS RESPONSE // Return HttpResponse object + | RETURNS MAPPING qualifiedName AS LIST_OF qualifiedName // Import mapping → list result + | RETURNS MAPPING qualifiedName AS qualifiedName // Import mapping → single object + | RETURNS NONE // Ignore response + | RETURNS NOTHING // Ignore response (alias) ; /** diff --git a/mdl/visitor/visitor_microflow_actions.go b/mdl/visitor/visitor_microflow_actions.go index dc095060..cf819382 100644 --- a/mdl/visitor/visitor_microflow_actions.go +++ b/mdl/visitor/visitor_microflow_actions.go @@ -1433,6 +1433,11 @@ func buildRestCallStatement(ctx parser.IRestCallStatementContext) *ast.RestCallS if len(qns) >= 2 { result.ResultEntity = buildQualifiedName(qns[1]) } + // `as list of Module.Entity` marks the mapping result as a list; + // without LIST_OF the result is a single object. + if returnsCtx.LIST_OF() != nil { + result.IsList = true + } } else if returnsCtx.NONE() != nil || returnsCtx.NOTHING() != nil { result.Type = ast.RestResultNone } diff --git a/sdk/mpr/parser_import_mapping.go b/sdk/mpr/parser_import_mapping.go index c9216ff9..91dfb0cd 100644 --- a/sdk/mpr/parser_import_mapping.go +++ b/sdk/mpr/parser_import_mapping.go @@ -105,6 +105,8 @@ func parseImportObjectMappingElement(raw map[string]any) *model.ImportMappingEle if v, ok := raw["Association"].(string); ok { elem.Association = v } + elem.MinOccurs = extractInt(raw["MinOccurs"]) + elem.MaxOccurs = extractInt(raw["MaxOccurs"]) // Parse children recursively (mix of object and value elements) if children, ok := raw["Children"].(bson.A); ok { @@ -141,6 +143,8 @@ func parseImportValueMappingElement(raw map[string]any) *model.ImportMappingElem if v, ok := raw["IsKey"].(bool); ok { elem.IsKey = v } + elem.MinOccurs = extractInt(raw["MinOccurs"]) + elem.MaxOccurs = extractInt(raw["MaxOccurs"]) // Extract the primitive type from the nested Type object if typeObj, ok := raw["Type"].(map[string]any); ok {