Skip to content

Commit d680f33

Browse files
committed
docs: nanoflow BSON mapping, Go example, test cases, and validation fixes
- Add Nanoflow Mapping section to BSON mapping documentation - Add create_nanoflow Go example with 4 nanoflow patterns - Update CHANGELOG with JS action syntax and association retrieve entries - Add nanoflow datasource option to create-page skill - Update SDK_EQUIVALENCE nanoflow feature list - Exhaustive nanoflow denylist test (21 disallowed + JS action allowed) - Document denylist ordering dependency in error handling - Fix isNumericLiteral: reject trailing dot ("5." → false)
1 parent bc131fe commit d680f33

12 files changed

Lines changed: 306 additions & 14 deletions

File tree

.claude/skills/mendix/create-page.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ column colActions (caption: 'Actions') {
343343
| `datasource: database from Module.Entity` | Direct database query |
344344
| `datasource: $Variable` | Variable bound (requires DATAVIEW parent with entity) |
345345
| `datasource: microflow Module.GetData()` | Microflow datasource |
346+
| `datasource: nanoflow Module.GetData()` | Nanoflow datasource (client-side, no server roundtrip) |
346347
| `datasource: selection widgetName` | Listen to selection from another widget |
347348
| `datasource: association path` | Retrieve by association from context (ByAssociation) |
348349
| `datasource: $currentObject/Module.Assoc` | Sugar for `association` — same semantics, reads more naturally |

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2020

2121
- **Nanoflow bug fixes** — Module existence validation for SHOW NANOFLOWS/MICROFLOWS, numeric return literals no longer get spurious `$` prefix, empty nanoflow/microflow names rejected at create time, `NanoflowCallAction` error handling type resolved correctly, `not()` expression spacing preserved on roundtrip, JavaScript action call rendering in DESCRIBE output
2222
- **Nanoflow diff support**`mxcli diff` now detects and displays nanoflow changes (previously silently skipped)
23+
- **JavaScript action MDL syntax**`call javascript action Module.ActionName(params)` now fully supported in CREATE NANOFLOW/MICROFLOW bodies: grammar, parser, builder, serializer, and roundtrip
24+
- **Association retrieve roundtrip fidelity**`retrieve $X from $Y/Module.Association` syntax preserved on roundtrip (previously converted to `from Entity where Assoc = $Y`)
2325
- **DESCRIBE empty-then optimization** — If/else blocks with empty true branches are swapped and condition negated for readable output
2426

2527
### Changed

docs/05-mdl-specification/10-bson-mapping.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,58 @@ Similar to CreateChangeAction but uses `microflows$ChangeAction`:
11941194

11951195
---
11961196

1197+
## Nanoflow Mapping
1198+
1199+
Nanoflows share the `Microflows` BSON domain — all action types, parameters, and flow structures are identical to microflows.
1200+
1201+
### BSON Type
1202+
1203+
| Unit Type | BSON `$Type` | Go Struct |
1204+
|-----------|-------------|-----------|
1205+
| Nanoflow | `Microflows$Nanoflow` | `microflows.Nanoflow` |
1206+
1207+
### Key Differences from Microflow
1208+
1209+
| Field | Microflow | Nanoflow |
1210+
|-------|-----------|----------|
1211+
| `$Type` | `Microflows$Microflow` | `Microflows$Nanoflow` |
1212+
| `AllowConcurrentExecution` | Present | Absent |
1213+
| `ConcurrencyErrorMicroflow` | Present | Absent |
1214+
| `ConcurrenyErrorMessage` | Present | Absent |
1215+
| `ApplyEntityAccess` | Present | Absent |
1216+
1217+
All other fields — `ObjectCollection`, `Parameters`, `ReturnType`, `AllowedModuleRoles`, `Documentation`, `Excluded`, `MarkAsUsed` — are identical.
1218+
1219+
### Allowed Actions
1220+
1221+
Nanoflows run client-side. The following action types are **disallowed** in nanoflows:
1222+
1223+
- `Microflows$CommitAction` — requires database (server-side)
1224+
- `Microflows$RollbackAction` — requires database
1225+
- `Microflows$DownloadFileAction` — requires server response
1226+
- `Microflows$ImportMappingCallAction` — requires server context
1227+
- `Microflows$ExportMappingCallAction` — requires server context
1228+
- `Microflows$RestCallAction` — use JavaScript actions for HTTP from client
1229+
- `Microflows$WebServiceCallAction` — server-side only
1230+
1231+
The following are **nanoflow-specific** (allowed only in nanoflows):
1232+
1233+
- `Microflows$SynchronizeAction` — triggers offline data sync
1234+
1235+
### JavaScript Action Calls
1236+
1237+
JavaScript actions use `Microflows$JavaScriptActionCallAction` (not `JavaActionCallAction`):
1238+
1239+
| Field | JavaAction | JavaScriptAction |
1240+
|-------|-----------|-----------------|
1241+
| `$Type` | `Microflows$JavaActionCallAction` | `Microflows$JavaScriptActionCallAction` |
1242+
| Action reference | `JavaAction` | `JavaScriptAction` |
1243+
| Result variable | `ResultVariableName` | `OutputVariableName` |
1244+
| Parameter mapping type | `Microflows$JavaActionParameterMapping` | `Microflows$JavaScriptActionParameterMapping` |
1245+
| Parameter value key | `Value` | `ParameterValue` |
1246+
1247+
---
1248+
11971249
## Debugging BSON Issues
11981250

11991251
When Studio Pro doesn't display data correctly (e.g., missing attributes, incorrect values), follow this debugging approach:

docs/11-proposals/SDK_EQUIVALENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ modelsdk-go/
153153
| Attribute types | ✅ Complete | 9 types |
154154
| Association CRUD | ✅ Complete | |
155155
| Microflow basic | ⚠️ Partial | Basic structure only |
156-
| Nanoflow CRUD | ⚠️ Partial | CREATE/DROP/DESCRIBE/SHOW/RENAME/MOVE, GRANT/REVOKE, diff support |
156+
| Nanoflow CRUD | ⚠️ Partial | CREATE/DROP/DESCRIBE/SHOW/RENAME/MOVE, GRANT/REVOKE, diff, JavaScript action calls, association retrieve roundtrip |
157157
| Page basic | ⚠️ Partial | Basic structure only |
158158
| JSON export | ✅ Complete | |
159159

docs/15-testing/nanoflow-test-cases.md

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ Total: 223 nanoflows across 3 projects.
2828
### 2. Build mxcli
2929

3030
```bash
31-
cd ~/workspace/mxcli
3231
git checkout pr4-nanoflows-all
3332
make build && make test && make lint-go
3433
```
3534

3635
### 3. Smoke test
3736

3837
```bash
39-
for mpr in ~/workspace/mendix-apps/*/*.mpr; do
38+
APPS_DIR=<path-to-extracted-apps>
39+
for mpr in "$APPS_DIR"/*/*.mpr; do
4040
echo "=== $(basename $(dirname $mpr)) ==="
4141
echo "show nanoflows;" > /tmp/show-nf.mdl
4242
mxcli exec /tmp/show-nf.mdl -p "$mpr" 2>&1 | tail -1
@@ -48,7 +48,7 @@ Expected: 79, 93, 51 nanoflows respectively.
4848
### 4. Interactive testing
4949

5050
```bash
51-
mxcli repl -p ~/workspace/mendix-apps/EnquiriesManagement/EnquiriesManagement.mpr
51+
mxcli repl -p <path-to-app>/EnquiriesManagement.mpr
5252
```
5353

5454
### 5. Script-based testing
@@ -57,7 +57,18 @@ mxcli repl -p ~/workspace/mendix-apps/EnquiriesManagement/EnquiriesManagement.mp
5757
mxcli exec test-sequence.mdl -p <mpr>
5858
```
5959

60-
Write operations (CREATE, DROP, GRANT/REVOKE) modify the `.mpr`. Back up before destructive tests.
60+
Write operations (CREATE, DROP, GRANT/REVOKE) modify the `.mpr` file **in place**.
61+
62+
> **IMPORTANT:** Always run destructive tests against a **copy** of the project folder,
63+
> never the original. The `.mpr` file references other files in the project directory,
64+
> and nanoflows that are DROPped cannot be recovered — there is no undo, no git history,
65+
> and no Studio Pro autosave for `.mpr` files.
66+
>
67+
> ```bash
68+
> # Before each destructive test session
69+
> cp -r MyProject MyProject-test
70+
> mxcli repl -p MyProject-test/MyProject.mpr
71+
> ```
6172
6273
---
6374
@@ -421,6 +432,37 @@ end;
421432
```
422433
**Expected:** Parses without error.
423434

435+
### 6.7 Call JavaScript action — simple
436+
```
437+
create nanoflow MyModule.JSTest () returns Boolean
438+
begin
439+
$Result = call javascript action NanoflowCommons.HasConnectivity ();
440+
end;
441+
```
442+
**Expected:** Parses, creates. DESCRIBE preserves `call javascript action` syntax.
443+
444+
### 6.8 Call JavaScript action — with parameters
445+
```
446+
create nanoflow MyModule.JSWithParams () returns Boolean
447+
begin
448+
$Result = call javascript action NanoflowCommons.SignIn (userName = 'test', password = 'pass');
449+
end;
450+
```
451+
**Expected:** Parameter mappings preserved in DESCRIBE.
452+
453+
### 6.9 Call JavaScript action — roundtrip
454+
1. DESCRIBE an existing nanoflow that calls a JavaScript action (e.g. `Atlas_Web_Content.ACT_Login`)
455+
2. Capture MDL output
456+
3. DROP the nanoflow
457+
4. Execute captured MDL (CREATE OR MODIFY)
458+
5. DESCRIBE again
459+
6. Compare — `call javascript action` syntax preserved. Only expected diff: `on error rollback` appended (default error handling)
460+
461+
### 6.10 Call JavaScript action — cross-module
462+
Test calling a JS action defined in a different module (e.g. `NanoflowCommons.SignIn` from `Atlas_Web_Content`).
463+
464+
**Expected:** Qualified action name preserved across modules.
465+
424466
---
425467

426468
## 7. GRANT / REVOKE EXECUTE ON NANOFLOW
@@ -689,13 +731,17 @@ create nanoflow M.BadReturn () returns NonExistent.MyEnum begin end;
689731
3. DESCRIBE — verify original version preserved
690732

691733
### 16.5 BSON roundtrip data integrity
692-
For 10+ complex nanoflows (error handling, annotations, 10+ activities, multiple parameter types):
734+
For 10+ complex nanoflows (error handling, annotations, 10+ activities, multiple parameter types, JavaScript action calls, association retrieves):
693735
1. DESCRIBE → capture
694736
2. DROP
695737
3. Execute captured MDL
696738
4. DESCRIBE → capture again
697739
5. Diff — any difference is a data loss bug
698740

741+
Include nanoflows with:
742+
- `call javascript action` actions (verify syntax preserved, not lost)
743+
- `retrieve $X from $Y/Module.Association` actions (verify association syntax preserved, not converted to database retrieve)
744+
699745
### 16.6 Double DROP
700746
```
701747
drop nanoflow M.X;

examples/create_nanoflow/main.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Example: Creating Nanoflows using MDL
4+
//
5+
// This example demonstrates how to create nanoflows programmatically
6+
// using the MDL (Mendix Definition Language) executor. Nanoflows run
7+
// client-side and support JavaScript action calls, show page/message
8+
// actions, and other client-compatible activities.
9+
//
10+
// There are two ways to create nanoflows:
11+
// 1. Using MDL via the mxcli command line (recommended for scripts)
12+
// 2. Using MDL programmatically via the executor (shown here)
13+
package main
14+
15+
import (
16+
"fmt"
17+
"os"
18+
"strings"
19+
20+
"github.com/mendixlabs/mxcli/mdl/backend"
21+
mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr"
22+
"github.com/mendixlabs/mxcli/mdl/executor"
23+
"github.com/mendixlabs/mxcli/mdl/visitor"
24+
)
25+
26+
func main() {
27+
if len(os.Args) < 2 {
28+
fmt.Println("Usage: create_nanoflow <path-to-mpr-file>")
29+
fmt.Println()
30+
fmt.Println("This example creates several nanoflows demonstrating different patterns.")
31+
fmt.Println("WARNING: This will modify the MPR file! Make a backup first.")
32+
fmt.Println()
33+
fmt.Println("Prerequisites:")
34+
fmt.Println(" - A module named 'MyModule' (or modify the code)")
35+
os.Exit(1)
36+
}
37+
38+
mprPath := os.Args[1]
39+
40+
// Create the MDL executor with stdout for output
41+
exec := executor.New(os.Stdout)
42+
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
43+
44+
// Define MDL script with several nanoflow examples
45+
mdlScript := fmt.Sprintf(`
46+
-- Connect to the Mendix project
47+
CONNECT '%s';
48+
49+
-- 1. Minimal nanoflow (no parameters, no return)
50+
create nanoflow MyModule.NF_HelloWorld ()
51+
begin
52+
show message 'Hello from a nanoflow!';
53+
end;
54+
55+
-- 2. Nanoflow with parameters and return type
56+
create nanoflow MyModule.NF_IsValidInput (Input : String) returns Boolean
57+
begin
58+
if length($Input) > 0 then
59+
return true;
60+
else
61+
return false;
62+
end if;
63+
end;
64+
65+
-- 3. Nanoflow calling a JavaScript action
66+
create nanoflow MyModule.NF_CheckConnectivity () returns Boolean
67+
begin
68+
$IsOnline = call javascript action NanoflowCommons.HasConnectivity ();
69+
return $IsOnline;
70+
end;
71+
72+
-- 4. Nanoflow calling another nanoflow
73+
create nanoflow MyModule.NF_ValidateAndCheck (Input : String) returns Boolean
74+
begin
75+
$IsValid = call nanoflow MyModule.NF_IsValidInput (Input = $Input);
76+
if $IsValid then
77+
$IsOnline = call nanoflow MyModule.NF_CheckConnectivity ();
78+
return $IsOnline;
79+
else
80+
return false;
81+
end if;
82+
end;
83+
84+
-- 5. Grant access to a role
85+
grant execute on nanoflow MyModule.NF_HelloWorld to MyModule.User;
86+
grant execute on nanoflow MyModule.NF_ValidateAndCheck to MyModule.User;
87+
88+
-- Verify: list all nanoflows in the module
89+
show nanoflows MyModule;
90+
91+
-- Verify: describe a nanoflow
92+
describe nanoflow MyModule.NF_ValidateAndCheck;
93+
94+
DISCONNECT;
95+
`, mprPath)
96+
97+
// Parse the MDL script
98+
fmt.Println("Parsing MDL script...")
99+
prog, errs := visitor.Build(mdlScript)
100+
if len(errs) > 0 {
101+
fmt.Printf("Parse errors:\n")
102+
for _, err := range errs {
103+
fmt.Printf(" - %v\n", err)
104+
}
105+
os.Exit(1)
106+
}
107+
108+
// Execute
109+
fmt.Println("\nExecuting MDL:")
110+
fmt.Println(strings.TrimSpace(mdlScript))
111+
fmt.Println()
112+
113+
err := exec.ExecuteProgram(prog)
114+
if err != nil {
115+
fmt.Printf("Error executing MDL: %v\n", err)
116+
fmt.Println("\nTip: Make sure the module exists in your project.")
117+
os.Exit(1)
118+
}
119+
120+
fmt.Println("\nNanoflows created successfully!")
121+
122+
// =========================================================================
123+
// Alternative: Using mxcli command line
124+
// =========================================================================
125+
fmt.Println("\n" + strings.Repeat("=", 60))
126+
fmt.Println("Alternative: Using mxcli command line")
127+
fmt.Println(strings.Repeat("=", 60))
128+
fmt.Println()
129+
fmt.Printf(" echo 'create nanoflow MyModule.NF_Test () begin show message '\"'\"'Hello!'\"'\"'; end;' | mxcli exec -p %s /dev/stdin\n", mprPath)
130+
fmt.Println()
131+
fmt.Println("Or with a script file:")
132+
fmt.Println()
133+
fmt.Printf(" mxcli exec -p %s nanoflows.mdl\n", mprPath)
134+
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ func isNumericLiteral(s string) bool {
11701170
return false
11711171
}
11721172
}
1173-
return hasDigit
1173+
return hasDigit && s[len(s)-1] != '.'
11741174
}
11751175

11761176
// formatImportXmlAction formats an import mapping action as MDL.

mdl/executor/cmd_microflows_format_action_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@ func TestIsNumericLiteral(t *testing.T) {
756756
{"$42", false},
757757
{"1.2.3", false},
758758
{"42abc", false},
759+
{".", false},
760+
{"-.", false},
761+
{"5.", false},
762+
{".5", true},
763+
{"-.5", true},
759764
}
760765
for _, tt := range tests {
761766
got := isNumericLiteral(tt.input)

mdl/executor/cmd_nanoflows_mock_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ func TestShowAccessOnNanoflow_Mock_NotFound(t *testing.T) {
303303
// --- NANOFLOW VALIDATION ---
304304

305305
func TestValidateNanoflowBody_DisallowedActions(t *testing.T) {
306+
// EXHAUSTIVE: every type in checkDisallowedNanoflowAction must appear here.
307+
// If a new server-side action is added to the AST but not to the denylist,
308+
// add it here so the test fails visibly.
306309
tests := []struct {
307310
name string
308311
stmt ast.MicroflowStatement
@@ -311,9 +314,24 @@ func TestValidateNanoflowBody_DisallowedActions(t *testing.T) {
311314
{"RaiseError", &ast.RaiseErrorStmt{}, "ErrorEvent"},
312315
{"JavaAction", &ast.CallJavaActionStmt{}, "Java"},
313316
{"DatabaseQuery", &ast.ExecuteDatabaseQueryStmt{}, "database"},
317+
{"CallExternalAction", &ast.CallExternalActionStmt{}, "external action"},
314318
{"ShowHomePage", &ast.ShowHomePageStmt{}, "SHOW HOME PAGE"},
315319
{"RestCall", &ast.RestCallStmt{}, "REST"},
320+
{"SendRestRequest", &ast.SendRestRequestStmt{}, "REST"},
321+
{"ImportFromMapping", &ast.ImportFromMappingStmt{}, "import mapping"},
322+
{"ExportToMapping", &ast.ExportToMappingStmt{}, "export mapping"},
323+
{"TransformJson", &ast.TransformJsonStmt{}, "JSON transformation"},
316324
{"CallWorkflow", &ast.CallWorkflowStmt{}, "workflow"},
325+
{"GetWorkflowData", &ast.GetWorkflowDataStmt{}, "workflow"},
326+
{"GetWorkflows", &ast.GetWorkflowsStmt{}, "workflow"},
327+
{"GetWorkflowActivityRecords", &ast.GetWorkflowActivityRecordsStmt{}, "workflow"},
328+
{"WorkflowOperation", &ast.WorkflowOperationStmt{}, "workflow"},
329+
{"SetTaskOutcome", &ast.SetTaskOutcomeStmt{}, "workflow"},
330+
{"OpenUserTask", &ast.OpenUserTaskStmt{}, "workflow"},
331+
{"NotifyWorkflow", &ast.NotifyWorkflowStmt{}, "workflow"},
332+
{"OpenWorkflow", &ast.OpenWorkflowStmt{}, "workflow"},
333+
{"LockWorkflow", &ast.LockWorkflowStmt{}, "workflow"},
334+
{"UnlockWorkflow", &ast.UnlockWorkflowStmt{}, "workflow"},
317335
}
318336

319337
for _, tt := range tests {
@@ -338,6 +356,7 @@ func TestValidateNanoflowBody_AllowedActions(t *testing.T) {
338356
{"ShowPage", &ast.ShowPageStmt{}},
339357
{"CallMicroflow", &ast.CallMicroflowStmt{}},
340358
{"CallNanoflow", &ast.CallNanoflowStmt{}},
359+
{"CallJavaScriptAction", &ast.CallJavaScriptActionStmt{}},
341360
{"CreateVariable", &ast.DeclareStmt{}},
342361
{"ChangeVariable", &ast.MfSetStmt{}},
343362
}

0 commit comments

Comments
 (0)