feat: add CREATE/DROP NANOFLOW support#7
feat: add CREATE/DROP NANOFLOW support#7retran wants to merge 1 commit intopr3-type-assertion-hardeningfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds end-to-end MDL support for creating and dropping nanoflows, mirroring the existing microflow pipeline (grammar → AST → visitor → executor registry/handlers), so MDL scripts can manage nanoflow documents in Mendix projects.
Changes:
- Extend the MDL grammar + generated parser listeners to parse
CREATE NANOFLOWandDROP NANOFLOW. - Introduce
CreateNanoflowStmt/DropNanoflowStmtAST nodes and corresponding visitor logic. - Add executor handlers for create/drop nanoflows, plus executor cache fields for created/dropped nanoflow tracking and registry wiring.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| mdl/grammar/MDLParser.g4 | Adds createNanoflowStatement and wires it into createStatement. |
| mdl/grammar/parser/mdlparser_listener.go | Regenerated listener interfaces to include nanoflow enter/exit hooks. |
| mdl/grammar/parser/mdlparser_base_listener.go | Regenerated base listener stubs for the new nanoflow production. |
| mdl/grammar/parser/mdl_lexer.go | Regenerated lexer header (ANTLR version bump). |
| mdl/ast/ast_microflow.go | Adds AST statement types for CREATE/DROP NANOFLOW. |
| mdl/visitor/visitor_microflow.go | Builds CreateNanoflowStmt from parse tree (mirrors microflow builder). |
| mdl/visitor/visitor_entity.go | Adds NANOFLOW branch to ExitDropStatement. |
| mdl/executor/register_stubs.go | Registers create/drop nanoflow handlers (currently under microflow handler registration). |
| mdl/executor/cmd_nanoflows_create.go | Implements execCreateNanoflow (mirrors microflow create flow builder + validations). |
| mdl/executor/cmd_nanoflows_drop.go | Implements execDropNanoflow (mirrors microflow drop behavior + dropped-ID tracking). |
| mdl/executor/executor.go | Extends executor cache structs and adds dropped-nanoflow tracking helpers. |
| mdl/executor/exec_context.go | Adds nanoflow “created during session” tracking helper. |
| mdl/executor/registry_test.go | Updates the known-statement snapshot list to include nanoflow statements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| createNanoflowStatement | ||
| : NANOFLOW qualifiedName | ||
| LPAREN microflowParameterList? RPAREN | ||
| microflowReturnType? | ||
| microflowOptions? | ||
| BEGIN microflowBody END SEMICOLON? SLASH? | ||
| ; |
There was a problem hiding this comment.
createNanoflowStatement is introduced without the explanatory doc block/examples that createMicroflowStatement has immediately above. If this grammar file is intended to be user-facing documentation as well (as it is for other statements), consider adding a short comment + example(s) for nanoflows to keep the CREATE statement docs consistent.
| // Build entity resolver function for parameter/return types | ||
| entityResolver := func(qn ast.QualifiedName) model.ID { | ||
| dms, err := ctx.Backend.ListDomainModels() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| modules, _ := ctx.Backend.ListModules() | ||
| moduleNames := make(map[model.ID]string) | ||
| for _, m := range modules { | ||
| moduleNames[m.ID] = m.Name | ||
| } |
There was a problem hiding this comment.
entityResolver silently swallows backend errors (ListDomainModels returns "" on error; ListModules error is ignored). This can surface as a misleading "entity not found" validation error when the real issue is a backend read failure. Prefer returning/propagating the backend error so users get an actionable failure reason.
| // Build entity resolver function for parameter/return types | |
| entityResolver := func(qn ast.QualifiedName) model.ID { | |
| dms, err := ctx.Backend.ListDomainModels() | |
| if err != nil { | |
| return "" | |
| } | |
| modules, _ := ctx.Backend.ListModules() | |
| moduleNames := make(map[model.ID]string) | |
| for _, m := range modules { | |
| moduleNames[m.ID] = m.Name | |
| } | |
| // Load metadata needed by the entity resolver up front so backend read | |
| // failures are returned as actionable errors instead of being treated as | |
| // "entity not found". | |
| dms, err := ctx.Backend.ListDomainModels() | |
| if err != nil { | |
| return mdlerrors.NewBackend("list domain models", err) | |
| } | |
| modules, err := ctx.Backend.ListModules() | |
| if err != nil { | |
| return mdlerrors.NewBackend("list modules", err) | |
| } | |
| moduleNames := make(map[model.ID]string) | |
| for _, m := range modules { | |
| moduleNames[m.ID] = m.Name | |
| } | |
| // Build entity resolver function for parameter/return types | |
| entityResolver := func(qn ast.QualifiedName) model.ID { |
| // execCreateNanoflow handles CREATE NANOFLOW statements. | ||
| func execCreateNanoflow(ctx *ExecContext, s *ast.CreateNanoflowStmt) error { | ||
| if !ctx.ConnectedForWrite() { | ||
| return mdlerrors.NewNotConnectedWrite() | ||
| } |
There was a problem hiding this comment.
execCreateNanoflow is very close to execCreateMicroflow (module/folder resolution, validation, flow graph build, create-or-replace semantics). Consider extracting shared helpers to reduce duplication and prevent microflow/nanoflow behavior drifting apart as fixes/features land over time.
| // execCreateNanoflow handles CREATE NANOFLOW statements. | ||
| func execCreateNanoflow(ctx *ExecContext, s *ast.CreateNanoflowStmt) error { | ||
| if !ctx.ConnectedForWrite() { | ||
| return mdlerrors.NewNotConnectedWrite() | ||
| } |
There was a problem hiding this comment.
There are extensive integration roundtrip tests for CREATE/DROP MICROFLOW, but this PR adds CREATE/DROP NANOFLOW without similar coverage. Adding at least one integration test that creates a nanoflow (and CREATE OR REPLACE) and then DESCRIBEs/drops it would help prevent regressions in the new executor handlers and dropped-ID reuse logic.
| // trackCreatedNanoflow registers a nanoflow created during this session. | ||
| func (ctx *ExecContext) trackCreatedNanoflow(moduleName, nfName string, id, containerID model.ID, returnEntityName string) { | ||
| ctx.ensureCache() | ||
| if ctx.Cache.createdNanoflows == nil { | ||
| ctx.Cache.createdNanoflows = make(map[string]*createdNanoflowInfo) | ||
| } | ||
| qualifiedName := moduleName + "." + nfName | ||
| ctx.Cache.createdNanoflows[qualifiedName] = &createdNanoflowInfo{ | ||
| ID: id, | ||
| Name: nfName, | ||
| ModuleName: moduleName, | ||
| ContainerID: containerID, | ||
| ReturnEntityName: returnEntityName, | ||
| } |
There was a problem hiding this comment.
trackCreatedNanoflow populates ctx.Cache.createdNanoflows, but there is no corresponding read path (no getCreatedNanoflow/resolver usage in the repo), so this cache currently adds state without affecting behavior. Either wire this cache into the relevant nanoflow resolvers (similar to createdMicroflows) or remove it to avoid dead code and future divergence.
| // trackCreatedNanoflow registers a nanoflow created during this session. | |
| func (ctx *ExecContext) trackCreatedNanoflow(moduleName, nfName string, id, containerID model.ID, returnEntityName string) { | |
| ctx.ensureCache() | |
| if ctx.Cache.createdNanoflows == nil { | |
| ctx.Cache.createdNanoflows = make(map[string]*createdNanoflowInfo) | |
| } | |
| qualifiedName := moduleName + "." + nfName | |
| ctx.Cache.createdNanoflows[qualifiedName] = &createdNanoflowInfo{ | |
| ID: id, | |
| Name: nfName, | |
| ModuleName: moduleName, | |
| ContainerID: containerID, | |
| ReturnEntityName: returnEntityName, | |
| } | |
| // trackCreatedNanoflow is kept for API compatibility, but nanoflow creations | |
| // are not cached because there is currently no corresponding read path for | |
| // this session state. | |
| func (ctx *ExecContext) trackCreatedNanoflow(moduleName, nfName string, id, containerID model.ID, returnEntityName string) { |
| } | ||
|
|
||
| // rememberDroppedNanoflow records the UnitID and ContainerID of a nanoflow | ||
| // that was just deleted so a subsequent CREATE OR REPLACE/MODIFY can reuse them. |
There was a problem hiding this comment.
The comment for rememberDroppedNanoflow says the nanoflow "was just deleted", but this function is called before deletion (to capture IDs). Consider aligning the wording with rememberDroppedMicroflow ("about to be deleted") to avoid misleading future maintainers.
| // that was just deleted so a subsequent CREATE OR REPLACE/MODIFY can reuse them. | |
| // that is about to be deleted so a subsequent CREATE OR REPLACE/MODIFY can reuse them. |
| func registerMicroflowHandlers(r *Registry) { | ||
| r.Register(&ast.CreateMicroflowStmt{}, func(ctx *ExecContext, stmt ast.Statement) error { | ||
| return execCreateMicroflow(ctx, stmt.(*ast.CreateMicroflowStmt)) | ||
| }) | ||
| r.Register(&ast.DropMicroflowStmt{}, func(ctx *ExecContext, stmt ast.Statement) error { | ||
| return execDropMicroflow(ctx, stmt.(*ast.DropMicroflowStmt)) | ||
| }) | ||
| r.Register(&ast.CreateNanoflowStmt{}, func(ctx *ExecContext, stmt ast.Statement) error { | ||
| return execCreateNanoflow(ctx, stmt.(*ast.CreateNanoflowStmt)) | ||
| }) | ||
| r.Register(&ast.DropNanoflowStmt{}, func(ctx *ExecContext, stmt ast.Statement) error { | ||
| return execDropNanoflow(ctx, stmt.(*ast.DropNanoflowStmt)) | ||
| }) | ||
| } |
There was a problem hiding this comment.
registerMicroflowHandlers now registers nanoflow handlers as well. To keep the registry organization self-descriptive, consider either renaming this function (e.g., microflow/nanoflow) or splitting nanoflow registration into its own helper alongside the other domain-specific registration functions.
db1a8b7 to
2356562
Compare
231606c to
fd6530f
Compare
2356562 to
4588cda
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 15 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // nanoflow bodies. These correspond to microflow-only actions in the Mendix | ||
| // runtime: Java actions, REST/web service calls, workflow actions, import/export, | ||
| // external object operations, download, push-to-client, show home page, and | ||
| // JSON transformation. |
There was a problem hiding this comment.
The comment above nanoflowDisallowedActions lists several disallowed categories (e.g., “download”, “push-to-client”, “external object operations”) that are not represented in the actual nanoflowDisallowedActions map. This makes the documentation misleading; either expand the map to include the missing AST statement types (if they exist in this codebase) or adjust the comment to only describe what is actually enforced.
| // nanoflow bodies. These correspond to microflow-only actions in the Mendix | |
| // runtime: Java actions, REST/web service calls, workflow actions, import/export, | |
| // external object operations, download, push-to-client, show home page, and | |
| // JSON transformation. | |
| // nanoflow bodies. These correspond to disallowed actions enforced here: | |
| // error events, Java actions, database queries, external action calls, | |
| // REST/web service calls, workflow actions, import/export mappings, | |
| // show home page, and JSON transformation. |
| // validateNanoflowReturnType checks that the return type is allowed for nanoflows. | ||
| // Binary and Float return types are not supported. | ||
| func validateNanoflowReturnType(retType *ast.MicroflowReturnType) string { | ||
| if retType == nil { | ||
| return "" | ||
| } | ||
| switch retType.Type.Kind { | ||
| case ast.TypeBinary: | ||
| return "Binary return type is not allowed in nanoflows" | ||
| } |
There was a problem hiding this comment.
validateNanoflowReturnType’s doc comment says “Binary and Float return types are not supported”, but the implementation only rejects ast.TypeBinary (and there is no TypeFloat in ast.DataTypeKind). Please either update the comment to match the actual rule, or add the missing return-type validation if another kind is intended to be disallowed.
| func validateNanoflowStatements(stmts []ast.MicroflowStatement, errors *[]string) { | ||
| for _, stmt := range stmts { | ||
| typeName := fmt.Sprintf("%T", stmt) | ||
| if reason, disallowed := nanoflowDisallowedActions[typeName]; disallowed { | ||
| *errors = append(*errors, reason) | ||
| continue |
There was a problem hiding this comment.
Using fmt.Sprintf("%T", stmt) + a map[string]string for disallowed-action matching is brittle (renames/refactors can silently break validation) and harder to make exhaustive. Consider switching to a type switch (grouping disallowed concrete types) or using reflect.Type keys so the compiler helps keep this list accurate.
| // Validate nanoflow-specific constraints before building the flow graph | ||
| qualName := s.Name.Module + "." + s.Name.Name | ||
| if errMsg := validateNanoflow(qualName, s.Body, s.ReturnType); errMsg != "" { | ||
| return fmt.Errorf("%s", errMsg) | ||
| } |
There was a problem hiding this comment.
There’s no executor test coverage for the new CREATE NANOFLOW handler and its nanoflow-specific validation (success paths + validation failures). Please add unit tests similar to the existing microflow create/drop mock-backend tests to prevent regressions (including CREATE OR REPLACE/MODIFY ID reuse after DROP).
fd6530f to
a1e8dad
Compare
4588cda to
bc2a53a
Compare
a1e8dad to
fd9950e
Compare
bc2a53a to
dabcdfb
Compare
fd9950e to
778f837
Compare
10312f1 to
2d18c34
Compare
6ef29ce to
c4e3655
Compare
2d18c34 to
0e18580
Compare
c4e3655 to
3b527e0
Compare
0e18580 to
dc11ba6
Compare
3b527e0 to
fac524a
Compare
dc11ba6 to
c13510c
Compare
fac524a to
72a49c8
Compare
c13510c to
75597c9
Compare
Why
mxcli supports CREATE/DROP for microflows but not nanoflows, leaving a gap in the MDL surface. Nanoflows are a core Mendix document type used heavily in client-side logic. Without this, users must fall back to Studio Pro for any nanoflow scaffolding — breaking the CLI-first workflow. Additionally, nanoflows have a restricted action palette and return type constraints that differ from microflows; these must be validated at CREATE time to prevent invalid models.
Summary
createNanoflowStatementgrammar rule inMDLParser.g4, reusingmicroflowParameterList,microflowReturnType,microflowOptions, andmicroflowBodyrulesCreateNanoflowStmtandDropNanoflowStmtAST typesExitCreateNanoflowStatementvisitor andNANOFLOWbranch inExitDropStatementexecCreateNanoflowandexecDropNanoflowexecutor handlers mirroring microflow pattern (withoutAllowedModuleRoles,AllowConcurrentExecution,ReturnVariableName— nanoflows don't have these fields)createdNanoflows,droppedNanoflows) and tracking helpers for DROP+CREATE rewrite safetynanoflow_validation.gowith nanoflow-specific validation:registerMicroflowHandlersallKnownStatementstest snapshotStacked on
Test
make build && make test && make lint-go— all pass