Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,35 @@ retrieve $Items from Module.Entity where Active = true;

**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).

## Legacy SOAP Web Service Calls

`call web service` preserves legacy Mendix SOAP activities. Prefer REST clients
for new integrations; this syntax exists mainly so existing projects can
round-trip without dropping SOAP actions.

```mdl
-- Structured form. DESCRIBE prefers Module.Document names when references are resolvable.
$Root = call web service 'SampleSOAP.OrderService'
operation 'FetchSampleItems'
send mapping 'SampleSOAP.OrderRequest'
receive mapping 'SampleSOAP.OrderResponse'
timeout 30
on error rollback;

-- Raw IDs are accepted when old project references are dangling or unavailable.
$Root = call web service 'sample-service-id'
operation 'FetchSampleItems'
send mapping 'sample-send-mapping-id'
receive mapping 'sample-receive-mapping-id';

-- Raw escape hatch emitted for unsupported SOAP fields.
$Root = call web service raw 'AQID';
```

**Design note:** the raw payload is base64-encoded BSON for the complete action
and is authoritative on re-exec. Treat this as round-trip support, not a
recommended authoring format for new integrations.

## REST Service Calls

MDL supports two patterns for calling REST APIs from microflows:
Expand Down
3 changes: 3 additions & 0 deletions cmd/mxcli/lsp_completions_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ authentication basic, session
| Retrieve (Assoc) | `retrieve $list from $Parent/Module.AssocName;` | Retrieve by association |
| Call microflow | `$Result = call microflow Module.Name (Param = $value);` | |
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
| Call web service | `$Result = call web service 'Module.Service' operation 'OperationName';` | Legacy SOAP; unresolved dangling refs fall back to raw IDs |
| Call web service raw | `$Result = call web service raw 'base64-bson';` | Escape hatch for byte-for-byte legacy SOAP round-trip |
| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` |
| Close page | `close page;` | |
| Validation | `validation feedback $entity/attribute message 'message';` | Requires attribute path + MESSAGE |
Expand Down
90 changes: 90 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_call_web_service_statement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Microflow Call Web Service Statement

Status: Draft

## Summary

Add MDL support for legacy Mendix SOAP `Microflows$CallWebServiceAction`.

```mdl
$Root = call web service 'SampleSOAP.OrderService'
operation 'FetchSampleItems'
send mapping 'SampleSOAP.OrderRequest'
receive mapping 'SampleSOAP.OrderResponse'
timeout 30;

$Root = call web service 'dangling-service-id'
operation 'FetchSampleItems'
send mapping 'dangling-send-mapping-id'
receive mapping 'dangling-receive-mapping-id';

$Root = call web service raw 'AQID';
```

This proposal is primarily about safe round-trip preservation of existing SOAP
actions. New integrations should prefer consumed REST services or inline REST
calls.

## Motivation

Legacy projects can contain SOAP web service calls. Without an MDL
representation, describe output either drops the activity or emits an
unsupported-action comment that cannot be re-executed into the same model.

The immediate goal is therefore fidelity:

- Parse existing `CallWebServiceAction` BSON.
- Emit an MDL statement that can be executed back into the MPR.
- Preserve unsupported or version-specific BSON fields when the structured
fields are incomplete.

## Syntax

```antlr
callWebServiceStatement
: (VARIABLE EQUALS)? CALL WEB SERVICE
(RAW STRING_LITERAL
| STRING_LITERAL
(OPERATION STRING_LITERAL)?
(SEND MAPPING STRING_LITERAL)?
(RECEIVE MAPPING STRING_LITERAL)?
(TIMEOUT expression)?)
onErrorClause?
;
```

## Design Notes

The structured form prefers stable qualified names for the imported web service
and mapping references. During `describe`, mxcli resolves known
`WebServices$ImportedWebService`, `ExportMappings$ExportMapping`, and
`ImportMappings$ImportMapping` IDs through the backend and emits
`Module.DocumentName`.

If a reference is dangling or the backend cannot resolve it, mxcli deliberately
falls back to the raw ID string so unsupported legacy projects still round-trip.

The `raw` form is an explicit escape hatch. Its string is base64-encoded BSON
for the complete action payload and is authoritative when re-executed. It exists
so unsupported SOAP fields can be preserved byte-for-byte until the structured
syntax covers them.

## Tests And Examples

- Parser/visitor coverage for structured and raw forms.
- Builder/writer coverage for real `WebServiceCallAction` construction and raw
BSON preservation.
- Formatter coverage for qualified-name resolution and raw-ID fallback.
- Example script: `mdl-examples/doctype-tests/call_web_service.test.mdl`.

## Resolved Questions

- Service and mapping references are emitted as `Module.Document` names when
the backend can resolve them. Raw IDs remain the fallback for dangling
references and incomplete project metadata.

## Open Questions

- Should the raw payload eventually move to a generic
`raw microflow action '...'` escape hatch instead of remaining under
`call web service raw`?
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ BSON schema Registry ◄──── multi-version Support
|----------|--------|---------|------------|
| [MDL Syntax Improvements v1](PROPOSAL_mdl_syntax_improvements.md) | Draft | Go-style assignment, C-style braces, fluent list APIs | — |
| [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 |
| [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 | — |
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) | — |
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done | — |
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
Expand Down
42 changes: 42 additions & 0 deletions mdl-examples/doctype-tests/call_web_service.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
create module SampleSOAP;

create entity SampleSOAP.OrderResponse (
Status : string(50)
);
/

create microflow SampleSOAP.ACT_FetchItems ()
returns SampleSOAP.OrderResponse as $Root
begin
$Root = call web service 'SampleSOAP.OrderService'
operation 'FetchSampleItems'
send mapping 'SampleSOAP.OrderRequest'
receive mapping 'SampleSOAP.OrderResponse'
timeout 30
on error rollback;

return $Root;
end;
/

create microflow SampleSOAP.ACT_FetchItemsDanglingRefs ()
returns SampleSOAP.OrderResponse as $Root
begin
-- Raw IDs are preserved when describe cannot resolve dangling legacy refs.
$Root = call web service 'sample-service-id'
operation 'FetchSampleItems'
send mapping 'sample-send-mapping-id'
receive mapping 'sample-receive-mapping-id';

return $Root;
end;
/

create microflow SampleSOAP.ACT_FetchItemsRaw ()
begin
-- Raw base64 escape hatch: opaque Microflows$CallWebServiceAction BSON is
-- preserved verbatim. The encoded payload here has no OutputVariable, so
-- no `$Var = ` assignment is emitted; richer payloads can carry one.
call web service raw 'uAAAAAIkSUQAGgAAAHNhbXBsZS13ZWItc2VydmljZS1hY3Rpb24AAiRUeXBlACAAAABNaWNyb2Zsb3dzJENhbGxXZWJTZXJ2aWNlQWN0aW9uAAJJbXBvcnRlZFNlcnZpY2UAEgAAAHNhbXBsZS1zZXJ2aWNlLWlkAAJPcGVyYXRpb25OYW1lABEAAABGZXRjaFNhbXBsZUl0ZW1zAAJUaW1lT3V0RXhwcmVzc2lvbgADAAAAMzAAAA==';
end;
/
15 changes: 15 additions & 0 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,21 @@ type CallJavaActionStmt struct {

func (s *CallJavaActionStmt) isMicroflowStatement() {}

// CallWebServiceStmt represents a legacy SOAP web service call.
type CallWebServiceStmt struct {
OutputVariable string // Optional output variable
RawBSONBase64 string // Raw Microflows$CallWebServiceAction BSON for lossless roundtrip
ServiceID string // Consumed web service ID or qualified name
OperationName string // Operation name
SendMappingID string // Optional export mapping ID or qualified name
ReceiveMappingID string // Optional import mapping ID or qualified name
Timeout Expression // Optional timeout expression
ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

func (s *CallWebServiceStmt) isMicroflowStatement() {}

// ExecuteDatabaseQueryStmt represents: EXECUTE DATABASE QUERY Module.Connection.QueryName ...
type ExecuteDatabaseQueryStmt struct {
OutputVariable string // Optional output variable
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_builder_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
return s.Annotations
case *ast.CallJavaActionStmt:
return s.Annotations
case *ast.CallWebServiceStmt:
return s.Annotations
case *ast.ExecuteDatabaseQueryStmt:
return s.Annotations
case *ast.CallExternalActionStmt:
Expand Down
107 changes: 107 additions & 0 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package executor

import (
"encoding/base64"
"fmt"
"log"
"strings"
Expand Down Expand Up @@ -268,6 +269,112 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
return activity.ID
}

// addCallWebServiceAction creates a legacy SOAP WebServiceCallAction.
func (fb *flowBuilder) addCallWebServiceAction(s *ast.CallWebServiceStmt) model.ID {
activityX := fb.posX
action := &microflows.WebServiceCallAction{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling),
ServiceID: model.ID(fb.resolveWebServiceRefForWrite(s.ServiceID)),
OperationName: s.OperationName,
SendMappingID: model.ID(fb.resolveMappingRefForWrite(s.SendMappingID, true)),
ReceiveMappingID: model.ID(fb.resolveMappingRefForWrite(s.ReceiveMappingID, false)),
OutputVariable: s.OutputVariable,
UseReturnVariable: s.OutputVariable != "",
}
if s.RawBSONBase64 != "" {
raw, err := base64.StdEncoding.DecodeString(s.RawBSONBase64)
if err != nil {
fb.addError("invalid raw web service action payload: %v", err)
} else {
action.RawBSON = raw
}
}
if s.Timeout != nil {
action.TimeoutExpression = fb.exprToString(s.Timeout)
}

activity := &microflows.ActionActivity{
BaseActivity: microflows.BaseActivity{
BaseMicroflowObject: microflows.BaseMicroflowObject{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Position: model.Point{X: fb.posX, Y: fb.posY},
Size: model.Size{Width: ActivityWidth, Height: ActivityHeight},
},
AutoGenerateCaption: true,
ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling),
},
Action: action,
}

fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing

if s.OutputVariable != "" && fb.declaredVars != nil {
fb.declaredVars[s.OutputVariable] = "Unknown"
}

if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
errorY := fb.posY + VerticalSpacing
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
}

return activity.ID
}

func (fb *flowBuilder) resolveWebServiceRefForWrite(ref string) string {
if ref == "" || !strings.Contains(ref, ".") || fb.backend == nil {
return ref
}
units, err := fb.backend.ListRawUnitsByType("WebServices$ImportedWebService")
if err != nil {
return ref
}
for _, unit := range units {
if unit == nil {
continue
}
name := rawUnitName(unit.Contents)
if name == "" {
continue
}
if fb.hierarchy != nil && fb.hierarchy.GetQualifiedName(unit.ContainerID, name) == ref {
return string(unit.ID)
}
if name == ref {
return string(unit.ID)
}
}
return ref
}

func (fb *flowBuilder) resolveMappingRefForWrite(ref string, preferExport bool) string {
if ref == "" || !strings.Contains(ref, ".") || fb.backend == nil {
return ref
}
moduleName, name, ok := strings.Cut(ref, ".")
if !ok || moduleName == "" || name == "" {
return ref
}
if preferExport {
if mapping, err := fb.backend.GetExportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
return string(mapping.ID)
}
if mapping, err := fb.backend.GetImportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
return string(mapping.ID)
}
} else {
if mapping, err := fb.backend.GetImportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
return string(mapping.ID)
}
if mapping, err := fb.backend.GetExportMappingByQualifiedName(moduleName, name); err == nil && mapping != nil {
return string(mapping.ID)
}
}
return ref
}

// addCallExternalActionAction creates a CALL EXTERNAL ACTION statement.
func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt) model.ID {
serviceQN := s.ServiceName.Module + "." + s.ServiceName.Name
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_builder_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ func (fb *flowBuilder) addStatement(stmt ast.MicroflowStatement) model.ID {
return fb.addCallMicroflowAction(s)
case *ast.CallJavaActionStmt:
return fb.addCallJavaActionAction(s)
case *ast.CallWebServiceStmt:
return fb.addCallWebServiceAction(s)
case *ast.ExecuteDatabaseQueryStmt:
return fb.addExecuteDatabaseQueryAction(s)
case *ast.CallExternalActionStmt:
Expand Down
8 changes: 8 additions & 0 deletions mdl/executor/cmd_microflows_builder_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ func (fb *flowBuilder) validateStatement(stmt ast.MicroflowStatement) {
fb.validateStatements(s.ErrorHandling.Body)
}

case *ast.CallWebServiceStmt:
if s.OutputVariable != "" {
fb.declaredVars[s.OutputVariable] = "Unknown"
}
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
fb.validateStatements(s.ErrorHandling.Body)
}

case *ast.ExecuteDatabaseQueryStmt:
if s.OutputVariable != "" {
fb.declaredVars[s.OutputVariable] = "Unknown"
Expand Down
Loading
Loading