Skip to content

Commit 460e1c0

Browse files
authored
Merge pull request #334 from hjotha/submit/microflow-call-web-service-statement
feat: support legacy microflow call web service statement
2 parents 78c42dc + 1860a9e commit 460e1c0

36 files changed

Lines changed: 16865 additions & 14973 deletions

.claude/skills/mendix/write-microflows.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,35 @@ retrieve $Items from Module.Entity where Active = true;
732732

733733
**Note**: `returns type as $Var` in the microflow signature does NOT create an activity variable — it only names the return value. So `$Var = call java action ...` after `returns as $Var` is fine (one creation).
734734

735+
## Legacy SOAP Web Service Calls
736+
737+
`call web service` preserves legacy Mendix SOAP activities. Prefer REST clients
738+
for new integrations; this syntax exists mainly so existing projects can
739+
round-trip without dropping SOAP actions.
740+
741+
```mdl
742+
-- Structured form. Resolved SOAP references use normal qualified names.
743+
$Root = call web service SampleSOAP.OrderService
744+
operation FetchSampleItems
745+
send mapping SampleSOAP.OrderRequest
746+
receive mapping SampleSOAP.OrderResponse
747+
timeout 30
748+
on error rollback;
749+
750+
-- Quoted raw IDs are accepted when old project references are dangling or unavailable.
751+
$Root = call web service 'sample-service-id'
752+
operation FetchSampleItems
753+
send mapping 'sample-send-mapping-id'
754+
receive mapping 'sample-receive-mapping-id';
755+
756+
-- Raw escape hatch emitted for unsupported SOAP fields.
757+
$Root = call web service raw 'AQID';
758+
```
759+
760+
**Design note:** the raw payload is base64-encoded BSON for the complete action
761+
and is authoritative on re-exec. Treat this as round-trip support, not a
762+
recommended authoring format for new integrations.
763+
735764
## REST Service Calls
736765

737766
MDL supports two patterns for calling REST APIs from microflows:

cmd/gen-completions/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,16 @@ func parseLexerGrammar(path string) ([]tokenEntry, error) {
133133
continue
134134
}
135135

136+
category := currentCategory
137+
if tokenName == "RECEIVE" {
138+
// RECEIVE is shared by REST and legacy SOAP call-web-service statements.
139+
category = "Service keyword"
140+
}
141+
136142
entries = append(entries, tokenEntry{
137143
Name: tokenName,
138144
Text: text,
139-
Category: currentCategory,
145+
Category: category,
140146
})
141147
continue
142148
}

cmd/mxcli/lsp_completions_gen.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ authentication basic, session
228228
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
229229
| Call JS action | `$Result = call javascript action Module.Name (Param = $value);` | JavaScript action (nanoflow/microflow) |
230230
| Call Java action | `$Result = call java action Module.Name (Param = $value);` | Java action (microflow only) |
231+
| Call web service | `$Result = call web service Module.Service operation OperationName;` | Legacy SOAP; quoted refs are fallback for dangling raw IDs |
232+
| Call web service raw | `$Result = call web service raw 'base64-bson';` | Escape hatch for byte-for-byte legacy SOAP round-trip |
231233
| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` |
232234
| Close page | `close page;` | |
233235
| Download file | `download file $FileDocument [show in browser];` | Streams a `System.FileDocument` |
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Microflow Call Web Service Statement
2+
3+
Status: Draft
4+
5+
## Summary
6+
7+
Add MDL support for legacy Mendix SOAP `Microflows$CallWebServiceAction`.
8+
9+
```mdl
10+
$Root = call web service SampleSOAP.OrderService
11+
operation FetchSampleItems
12+
send mapping SampleSOAP.OrderRequest
13+
receive mapping SampleSOAP.OrderResponse
14+
timeout 30;
15+
16+
$Root = call web service 'dangling-service-id'
17+
operation FetchSampleItems
18+
send mapping 'dangling-send-mapping-id'
19+
receive mapping 'dangling-receive-mapping-id';
20+
21+
$Root = call web service raw 'AQID';
22+
```
23+
24+
This proposal is primarily about safe round-trip preservation of existing SOAP
25+
actions. New integrations should prefer consumed REST services or inline REST
26+
calls.
27+
28+
## Motivation
29+
30+
Legacy projects can contain SOAP web service calls. Without an MDL
31+
representation, describe output either drops the activity or emits an
32+
unsupported-action comment that cannot be re-executed into the same model.
33+
34+
The immediate goal is therefore fidelity:
35+
36+
- Parse existing `CallWebServiceAction` BSON.
37+
- Emit an MDL statement that can be executed back into the MPR.
38+
- Preserve unsupported or version-specific BSON fields when the structured
39+
fields are incomplete.
40+
41+
## Syntax
42+
43+
```antlr
44+
callWebServiceStatement
45+
: (VARIABLE EQUALS)? CALL WEB SERVICE
46+
(RAW STRING_LITERAL
47+
| webServiceReference
48+
(OPERATION webServiceReference)?
49+
(SEND MAPPING webServiceReference)?
50+
(RECEIVE MAPPING webServiceReference)?
51+
(TIMEOUT expression)?)
52+
onErrorClause?
53+
;
54+
55+
webServiceReference
56+
: qualifiedName
57+
| STRING_LITERAL
58+
;
59+
```
60+
61+
## Design Notes
62+
63+
The structured form prefers stable qualified names for the imported web service
64+
and mapping references. During `describe`, mxcli resolves known
65+
`WebServices$ImportedWebService`, `ExportMappings$ExportMapping`, and
66+
`ImportMappings$ImportMapping` IDs through the backend and emits
67+
`Module.DocumentName`.
68+
69+
If a reference is dangling or the backend cannot resolve it, mxcli deliberately
70+
falls back to a quoted raw ID string so unsupported legacy projects still
71+
round-trip without pretending the ID is a normal document name.
72+
73+
The `raw` form is an explicit escape hatch. Its string is base64-encoded BSON
74+
for the complete action payload and is authoritative when re-executed. It exists
75+
so unsupported SOAP fields can be preserved byte-for-byte until the structured
76+
syntax covers them.
77+
78+
## Tests And Examples
79+
80+
- Parser/visitor coverage for structured and raw forms.
81+
- Builder/writer coverage for real `WebServiceCallAction` construction and raw
82+
BSON preservation.
83+
- Formatter coverage for qualified-name resolution and raw-ID fallback.
84+
- Example script: `mdl-examples/doctype-tests/call_web_service.mdl`.
85+
86+
## Resolved Questions
87+
88+
- Service and mapping references are emitted as `Module.Document` names when
89+
the backend can resolve them. Raw IDs remain quoted fallback references for
90+
dangling references and incomplete project metadata.
91+
- Structured resolved references use `qualifiedName` tokens for consistency
92+
with other MDL document references. `STRING_LITERAL` is only the fallback for
93+
dangling raw IDs and names that cannot be emitted as bare identifiers.
94+
95+
## Open Questions
96+
97+
- Should the raw payload eventually move to a generic
98+
`raw microflow action '...'` escape hatch instead of remaining under
99+
`call web service raw`?

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ BSON schema Registry ◄──── multi-version Support
4545
| [MDL Syntax Improvements v2](PROPOSAL_mdl_syntax_improvements_v2.md) | Proposed | Consolidated v2: unified variable declaration, C-style braces, fluent list ops | Syntax Improvements v1 |
4646
| [Microflow Free Annotation](PROPOSAL_microflow_free_annotation.md) | Draft | Order-sensitive `@annotation` handling for free-floating visual notes in microflows ||
4747
| [Microflow Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring ||
48+
| [Microflow Call Web Service Statement](PROPOSAL_microflow_call_web_service_statement.md) | Draft | Structured and raw MDL syntax for legacy SOAP `CallWebServiceAction` round-trip preservation ||
4849
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) ||
4950
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done ||
5051
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
create module SampleSOAP;
2+
3+
create entity SampleSOAP.OrderResponse (
4+
Status : string(50)
5+
);
6+
/
7+
8+
create microflow SampleSOAP.ACT_FetchItems ()
9+
returns SampleSOAP.OrderResponse as $Root
10+
begin
11+
$Root = call web service SampleSOAP.OrderService
12+
operation FetchSampleItems
13+
send mapping SampleSOAP.OrderRequest
14+
receive mapping SampleSOAP.OrderResponse
15+
timeout 30
16+
on error rollback;
17+
18+
return $Root;
19+
end;
20+
/
21+
22+
create microflow SampleSOAP.ACT_FetchItemsDanglingRefs ()
23+
returns SampleSOAP.OrderResponse as $Root
24+
begin
25+
-- Quoted raw IDs are preserved when describe cannot resolve dangling legacy refs.
26+
$Root = call web service 'sample-service-id'
27+
operation FetchSampleItems
28+
send mapping 'sample-send-mapping-id'
29+
receive mapping 'sample-receive-mapping-id';
30+
31+
return $Root;
32+
end;
33+
/
34+
35+
create microflow SampleSOAP.ACT_FetchItemsRaw ()
36+
begin
37+
-- Raw base64 escape hatch: opaque Microflows$CallWebServiceAction BSON is
38+
-- preserved verbatim. The encoded payload here has no OutputVariable, so
39+
-- no `$Var = ` assignment is emitted; richer payloads can carry one.
40+
call web service raw 'uAAAAAIkSUQAGgAAAHNhbXBsZS13ZWItc2VydmljZS1hY3Rpb24AAiRUeXBlACAAAABNaWNyb2Zsb3dzJENhbGxXZWJTZXJ2aWNlQWN0aW9uAAJJbXBvcnRlZFNlcnZpY2UAEgAAAHNhbXBsZS1zZXJ2aWNlLWlkAAJPcGVyYXRpb25OYW1lABEAAABGZXRjaFNhbXBsZUl0ZW1zAAJUaW1lT3V0RXhwcmVzc2lvbgADAAAAMzAAAA==';
41+
end;
42+
/

mdl/ast/ast_microflow.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,21 @@ type CallJavaScriptActionStmt struct {
381381

382382
func (s *CallJavaScriptActionStmt) isMicroflowStatement() {}
383383

384+
// CallWebServiceStmt represents a legacy SOAP web service call.
385+
type CallWebServiceStmt struct {
386+
OutputVariable string // Optional output variable
387+
RawBSONBase64 string // Raw Microflows$CallWebServiceAction BSON for lossless roundtrip
388+
ServiceID string // Consumed web service ID or qualified name
389+
OperationName string // Operation name
390+
SendMappingID string // Optional export mapping ID or qualified name
391+
ReceiveMappingID string // Optional import mapping ID or qualified name
392+
Timeout Expression // Optional timeout expression
393+
ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause
394+
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
395+
}
396+
397+
func (s *CallWebServiceStmt) isMicroflowStatement() {}
398+
384399
// ExecuteDatabaseQueryStmt represents: EXECUTE DATABASE QUERY Module.Connection.QueryName ...
385400
type ExecuteDatabaseQueryStmt struct {
386401
OutputVariable string // Optional output variable

mdl/executor/cmd_microflows_builder_annotations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
4949
return s.Annotations
5050
case *ast.CallJavaScriptActionStmt:
5151
return s.Annotations
52+
case *ast.CallWebServiceStmt:
53+
return s.Annotations
5254
case *ast.ExecuteDatabaseQueryStmt:
5355
return s.Annotations
5456
case *ast.CallExternalActionStmt:

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package executor
55

66
import (
7+
"encoding/base64"
78
"fmt"
89
"log"
910
"strings"
@@ -437,6 +438,86 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
437438
return activity.ID
438439
}
439440

441+
// addCallWebServiceAction creates a legacy SOAP WebServiceCallAction.
442+
func (fb *flowBuilder) addCallWebServiceAction(s *ast.CallWebServiceStmt) model.ID {
443+
activityX := fb.posX
444+
action := &microflows.WebServiceCallAction{
445+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
446+
ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling),
447+
ServiceID: model.ID(s.ServiceID),
448+
OperationName: s.OperationName,
449+
SendMappingID: model.ID(fb.resolveMappingRefForWrite(s.SendMappingID, true)),
450+
ReceiveMappingID: model.ID(fb.resolveMappingRefForWrite(s.ReceiveMappingID, false)),
451+
OutputVariable: s.OutputVariable,
452+
UseReturnVariable: s.OutputVariable != "",
453+
}
454+
if s.RawBSONBase64 != "" {
455+
raw, err := base64.StdEncoding.DecodeString(s.RawBSONBase64)
456+
if err != nil {
457+
fb.addError("invalid raw web service action payload: %v", err)
458+
} else {
459+
action.RawBSON = raw
460+
}
461+
}
462+
if s.Timeout != nil {
463+
action.TimeoutExpression = fb.exprToString(s.Timeout)
464+
}
465+
466+
activity := &microflows.ActionActivity{
467+
BaseActivity: microflows.BaseActivity{
468+
BaseMicroflowObject: microflows.BaseMicroflowObject{
469+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
470+
Position: model.Point{X: fb.posX, Y: fb.posY},
471+
Size: model.Size{Width: ActivityWidth, Height: ActivityHeight},
472+
},
473+
AutoGenerateCaption: true,
474+
ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling),
475+
},
476+
Action: action,
477+
}
478+
479+
fb.objects = append(fb.objects, activity)
480+
fb.posX += fb.spacing
481+
482+
if s.OutputVariable != "" && fb.declaredVars != nil {
483+
fb.declaredVars[s.OutputVariable] = "Unknown"
484+
}
485+
486+
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
487+
errorY := fb.posY + VerticalSpacing
488+
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
489+
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
490+
}
491+
492+
return activity.ID
493+
}
494+
495+
func (fb *flowBuilder) resolveMappingRefForWrite(ref string, preferExport bool) string {
496+
if ref == "" || !strings.Contains(ref, ".") || fb.backend == nil {
497+
return ref
498+
}
499+
moduleName, name, ok := strings.Cut(ref, ".")
500+
if !ok || moduleName == "" || name == "" {
501+
return ref
502+
}
503+
if preferExport {
504+
if mapping, err := fb.backend.GetExportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
505+
return string(mapping.ID)
506+
}
507+
if mapping, err := fb.backend.GetImportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
508+
return string(mapping.ID)
509+
}
510+
} else {
511+
if mapping, err := fb.backend.GetImportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
512+
return string(mapping.ID)
513+
}
514+
if mapping, err := fb.backend.GetExportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
515+
return string(mapping.ID)
516+
}
517+
}
518+
return ref
519+
}
520+
440521
// addCallExternalActionAction creates a CALL EXTERNAL ACTION statement.
441522
func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt) model.ID {
442523
serviceQN := s.ServiceName.Module + "." + s.ServiceName.Name

0 commit comments

Comments
 (0)