Skip to content

Commit a1268fb

Browse files
hjothamendixclaude
andcommitted
fix: infer SingleObject from import mapping root when JsonStructure absent
addRestCallAction and addImportFromMappingAction decide whether the mapping result is a single object or a list by looking up the JSON structure the import mapping references. For mappings backed by an XML schema or message definition the mapping has no JsonStructure, so the JSON-structure lookup short-circuits and SingleObject defaults to false (REST) or stays true (import-from-mapping). When the authored mapping is rooted on an Object, the resulting BSON ResultHandling type (ListType vs ObjectType) and Range.SingleObject flag mismatch the microflow's ObjectType return, surfacing as CE0117 "Error in expression" at the End event (and CE0019/CE0136/CE0243 cascades on any downstream retrieve over the misclassified variable). Fall back to the import mapping's own root element kind when JsonStructure is empty: ImportMappingElement.Kind is "Object", "Array", or "Value" — Studio Pro authors it the same way for both JSON-backed and schema/message-definition mappings, so a single source of truth at the mapping's first element correctly recovers the shape for both code paths. Two regression tests cover the new fallback (Object → SingleObject=true, Array → SingleObject=false) using synthetic Module.Mapping names; the existing JSON-structure-driven test still asserts the prior path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f96a037 commit a1268fb

2 files changed

Lines changed: 124 additions & 8 deletions

File tree

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,18 +1025,31 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID {
10251025
// MDL did not explicitly assign one.
10261026
s.OutputVariable = s.Result.ResultEntity.Name
10271027
}
1028-
// Determine whether the import mapping returns a single object or a list by
1029-
// looking at the JSON structure it references. If the root JSON element is
1030-
// an Object, the mapping produces one object; if it is an Array, a list.
1028+
// Determine whether the import mapping returns a single object or a list.
1029+
// First try the JSON structure it references — if the root JSON element
1030+
// is an Object, the mapping produces one object; if it is an Array, a
1031+
// list. When the mapping is backed by an XML schema or message
1032+
// definition (no JsonStructure set), fall back to the import mapping's
1033+
// own root element kind, which Studio Pro authors as "Object" or
1034+
// "Array" the same way.
10311035
singleObject := false
10321036
if fb.backend != nil {
1033-
if im, err := fb.backend.GetImportMappingByQualifiedName(s.Result.MappingName.Module, s.Result.MappingName.Name); err == nil && im.JsonStructure != "" {
1034-
// im.JsonStructure is "Module.Name" — split and look up the JSON structure.
1035-
if parts := strings.SplitN(im.JsonStructure, ".", 2); len(parts) == 2 {
1036-
if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 {
1037-
singleObject = js.Elements[0].ElementType == "Object"
1037+
if im, err := fb.backend.GetImportMappingByQualifiedName(s.Result.MappingName.Module, s.Result.MappingName.Name); err == nil {
1038+
resolved := false
1039+
if im.JsonStructure != "" {
1040+
// im.JsonStructure is "Module.Name" — split and look up the JSON structure.
1041+
if parts := strings.SplitN(im.JsonStructure, ".", 2); len(parts) == 2 {
1042+
if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 {
1043+
singleObject = js.Elements[0].ElementType == "Object"
1044+
resolved = true
1045+
}
10381046
}
10391047
}
1048+
if !resolved && len(im.Elements) > 0 && im.Elements[0] != nil {
1049+
// XML schema / message-definition mappings carry the
1050+
// single-vs-list shape on the root mapping element itself.
1051+
singleObject = im.Elements[0].Kind == "Object"
1052+
}
10401053
}
10411054
}
10421055
resultHandling = &microflows.ResultHandlingMapping{
@@ -1318,19 +1331,27 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt)
13181331
}
13191332

13201333
// Determine single vs list and result entity from the import mapping.
1334+
// JSON structure check covers JSON-backed mappings; for XML schema or
1335+
// message-definition mappings JsonStructure is empty and the root
1336+
// element kind on the mapping itself indicates Array vs Object.
13211337
resultEntityQN := ""
13221338
if fb.backend != nil {
13231339
if im, err := fb.backend.GetImportMappingByQualifiedName(s.Mapping.Module, s.Mapping.Name); err == nil {
1340+
resolved := false
13241341
if im.JsonStructure != "" {
13251342
parts := strings.SplitN(im.JsonStructure, ".", 2)
13261343
if len(parts) == 2 {
13271344
if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 {
13281345
if js.Elements[0].ElementType == "Array" {
13291346
resultHandling.SingleObject = false
13301347
}
1348+
resolved = true
13311349
}
13321350
}
13331351
}
1352+
if !resolved && len(im.Elements) > 0 && im.Elements[0] != nil && im.Elements[0].Kind == "Array" {
1353+
resultHandling.SingleObject = false
1354+
}
13341355
if len(im.Elements) > 0 && im.Elements[0].Entity != "" {
13351356
resultEntityQN = im.Elements[0].Entity
13361357
resultHandling.ResultEntityID = model.ID(resultEntityQN)

mdl/executor/cmd_microflows_builder_rest_response_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,101 @@ func TestAddRestCallAction_ReturnsResponseUsesHttpResponseHandling(t *testing.T)
5858
}
5959
}
6060

61+
// REST call mappings backed by an XML schema or message definition (no
62+
// JsonStructure set) must still infer single-vs-list from the import
63+
// mapping's own root element kind. Otherwise the builder defaults to
64+
// SingleObject=false and emits a ListType result, which mismatches the
65+
// authored ObjectType return and triggers CE0117 / CE0019 / CE0136
66+
// downstream when the microflow's return value references the result.
67+
func TestAddRestCallAction_MappingFallsBackToImportMappingRootKindWhenJsonStructureMissing(t *testing.T) {
68+
fb := &flowBuilder{
69+
posX: 100,
70+
posY: 100,
71+
spacing: HorizontalSpacing,
72+
varTypes: map[string]string{},
73+
declaredVars: map[string]string{},
74+
measurer: &layoutMeasurer{},
75+
backend: &mock.MockBackend{
76+
GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) {
77+
if moduleName != "Synthetic" || name != "MsgDefMapping" {
78+
return nil, fmt.Errorf("unexpected import mapping %s.%s", moduleName, name)
79+
}
80+
return &model.ImportMapping{
81+
Name: "MsgDefMapping",
82+
// Empty JsonStructure simulates an XML-schema or message-
83+
// definition backed mapping.
84+
JsonStructure: "",
85+
Elements: []*model.ImportMappingElement{
86+
{Kind: "Object", Entity: "Synthetic.Item"},
87+
},
88+
}, nil
89+
},
90+
},
91+
}
92+
93+
stmt := &ast.RestCallStmt{
94+
OutputVariable: "Item",
95+
Method: ast.HttpMethodGet,
96+
URL: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "https://example.com"},
97+
Result: ast.RestResult{
98+
Type: ast.RestResultMapping,
99+
MappingName: ast.QualifiedName{Module: "Synthetic", Name: "MsgDefMapping"},
100+
ResultEntity: ast.QualifiedName{Module: "Synthetic", Name: "Item"},
101+
},
102+
}
103+
fb.addRestCallAction(stmt)
104+
105+
activity := fb.objects[0].(*microflows.ActionActivity)
106+
action := activity.Action.(*microflows.RestCallAction)
107+
mapping := action.ResultHandling.(*microflows.ResultHandlingMapping)
108+
if !mapping.SingleObject {
109+
t.Errorf("SingleObject = false, want true (root mapping element Kind=Object)")
110+
}
111+
}
112+
113+
// And the inverse: an Array root on the mapping element must yield a
114+
// list-typed result handling.
115+
func TestAddRestCallAction_MappingFallsBackToArrayKindWhenJsonStructureMissing(t *testing.T) {
116+
fb := &flowBuilder{
117+
posX: 100,
118+
posY: 100,
119+
spacing: HorizontalSpacing,
120+
varTypes: map[string]string{},
121+
declaredVars: map[string]string{},
122+
measurer: &layoutMeasurer{},
123+
backend: &mock.MockBackend{
124+
GetImportMappingByQualifiedNameFunc: func(moduleName, name string) (*model.ImportMapping, error) {
125+
return &model.ImportMapping{
126+
Name: "ArrMapping",
127+
JsonStructure: "",
128+
Elements: []*model.ImportMappingElement{
129+
{Kind: "Array", Entity: "Synthetic.Item"},
130+
},
131+
}, nil
132+
},
133+
},
134+
}
135+
136+
stmt := &ast.RestCallStmt{
137+
OutputVariable: "Items",
138+
Method: ast.HttpMethodGet,
139+
URL: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "https://example.com"},
140+
Result: ast.RestResult{
141+
Type: ast.RestResultMapping,
142+
MappingName: ast.QualifiedName{Module: "Synthetic", Name: "ArrMapping"},
143+
ResultEntity: ast.QualifiedName{Module: "Synthetic", Name: "Item"},
144+
},
145+
}
146+
fb.addRestCallAction(stmt)
147+
148+
activity := fb.objects[0].(*microflows.ActionActivity)
149+
action := activity.Action.(*microflows.RestCallAction)
150+
mapping := action.ResultHandling.(*microflows.ResultHandlingMapping)
151+
if mapping.SingleObject {
152+
t.Errorf("SingleObject = true, want false (root mapping element Kind=Array)")
153+
}
154+
}
155+
61156
func TestAddRestCallAction_MappingResultPreservesExplicitOutputVariable(t *testing.T) {
62157
fb := &flowBuilder{
63158
posX: 100,

0 commit comments

Comments
 (0)