Skip to content
Merged
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 @@ -732,6 +732,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. Resolved SOAP references use normal qualified names.
$Root = call web service SampleSOAP.OrderService
operation FetchSampleItems
send mapping SampleSOAP.OrderRequest
receive mapping SampleSOAP.OrderResponse
timeout 30
on error rollback;

-- Quoted 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
8 changes: 7 additions & 1 deletion cmd/gen-completions/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,16 @@ func parseLexerGrammar(path string) ([]tokenEntry, error) {
continue
}

category := currentCategory
if tokenName == "RECEIVE" {
// RECEIVE is shared by REST and legacy SOAP call-web-service statements.
category = "Service keyword"
}

entries = append(entries, tokenEntry{
Name: tokenName,
Text: text,
Category: currentCategory,
Category: category,
})
continue
}
Expand Down
7 changes: 7 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 @@ -228,6 +228,8 @@ authentication basic, session
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
| Call JS action | `$Result = call javascript action Module.Name (Param = $value);` | JavaScript action (nanoflow/microflow) |
| 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 |
| 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` |
Expand Down
99 changes: 99 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,99 @@
# 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
| webServiceReference
(OPERATION webServiceReference)?
(SEND MAPPING webServiceReference)?
(RECEIVE MAPPING webServiceReference)?
(TIMEOUT expression)?)
onErrorClause?
;

webServiceReference
: qualifiedName
| STRING_LITERAL
;
```

## 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 a quoted raw ID string so unsupported legacy projects still
round-trip without pretending the ID is a normal document name.

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.mdl`.

## Resolved Questions

- Service and mapping references are emitted as `Module.Document` names when
the backend can resolve them. Raw IDs remain quoted fallback references for
dangling references and incomplete project metadata.
- Structured resolved references use `qualifiedName` tokens for consistency
with other MDL document references. `STRING_LITERAL` is only the fallback for
dangling raw IDs and names that cannot be emitted as bare identifiers.

## 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 @@ -45,6 +45,7 @@ BSON schema Registry ◄──── multi-version Support
| [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 Free Annotation](PROPOSAL_microflow_free_annotation.md) | Draft | Order-sensitive `@annotation` handling for free-floating visual notes in microflows | — |
| [Microflow Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring | — |
| [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.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
-- Quoted 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 @@ -381,6 +381,21 @@ type CallJavaScriptActionStmt struct {

func (s *CallJavaScriptActionStmt) 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 @@ -49,6 +49,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
return s.Annotations
case *ast.CallJavaScriptActionStmt:
return s.Annotations
case *ast.CallWebServiceStmt:
return s.Annotations
case *ast.ExecuteDatabaseQueryStmt:
return s.Annotations
case *ast.CallExternalActionStmt:
Expand Down
81 changes: 81 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 @@ -437,6 +438,86 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
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(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) 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 @@ -486,6 +486,8 @@ func (fb *flowBuilder) addStatement(stmt ast.MicroflowStatement) model.ID {
return fb.addCallJavaActionAction(s)
case *ast.CallJavaScriptActionStmt:
return fb.addCallJavaScriptActionAction(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 @@ -184,6 +184,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