Skip to content

feat: add CREATE/DROP NANOFLOW support#7

Open
retran wants to merge 1 commit intopr3-type-assertion-hardeningfrom
pr4-nanoflows-create-drop
Open

feat: add CREATE/DROP NANOFLOW support#7
retran wants to merge 1 commit intopr3-type-assertion-hardeningfrom
pr4-nanoflows-create-drop

Conversation

@retran
Copy link
Copy Markdown
Owner

@retran retran commented Apr 23, 2026

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

  • Add createNanoflowStatement grammar rule in MDLParser.g4, reusing microflowParameterList, microflowReturnType, microflowOptions, and microflowBody rules
  • Add CreateNanoflowStmt and DropNanoflowStmt AST types
  • Add ExitCreateNanoflowStatement visitor and NANOFLOW branch in ExitDropStatement
  • Add execCreateNanoflow and execDropNanoflow executor handlers mirroring microflow pattern (without AllowedModuleRoles, AllowConcurrentExecution, ReturnVariableName — nanoflows don't have these fields)
  • Add nanoflow cache fields (createdNanoflows, droppedNanoflows) and tracking helpers for DROP+CREATE rewrite safety
  • Add nanoflow_validation.go with nanoflow-specific validation:
    • Reject 21 microflow-only action types (Java actions, REST calls, workflow actions, import/export, external actions, show home page, JSON transform, ErrorEvent)
    • Reject Binary return type (nanoflows cannot return Binary)
    • Recursive validation through compound statements (if/loop/while) and error handling bodies
  • Register nanoflow handlers in registerMicroflowHandlers
  • Update allKnownStatements test snapshot

Stacked on

Test

make build && make test && make lint-go — all pass

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NANOFLOW and DROP NANOFLOW.
  • Introduce CreateNanoflowStmt / DropNanoflowStmt AST 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.

Comment thread mdl/grammar/MDLParser.g4
Comment on lines +1172 to +1178
createNanoflowStatement
: NANOFLOW qualifiedName
LPAREN microflowParameterList? RPAREN
microflowReturnType?
microflowOptions?
BEGIN microflowBody END SEMICOLON? SLASH?
;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +94
// 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
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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 {

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
// execCreateNanoflow handles CREATE NANOFLOW statements.
func execCreateNanoflow(ctx *ExecContext, s *ast.CreateNanoflowStmt) error {
if !ctx.ConnectedForWrite() {
return mdlerrors.NewNotConnectedWrite()
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
// execCreateNanoflow handles CREATE NANOFLOW statements.
func execCreateNanoflow(ctx *ExecContext, s *ast.CreateNanoflowStmt) error {
if !ctx.ConnectedForWrite() {
return mdlerrors.NewNotConnectedWrite()
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +175
// 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,
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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) {

Copilot uses AI. Check for mistakes.
Comment thread mdl/executor/executor.go
}

// rememberDroppedNanoflow records the UnitID and ContainerID of a nanoflow
// that was just deleted so a subsequent CREATE OR REPLACE/MODIFY can reuse them.
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines 87 to 100
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))
})
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@retran retran force-pushed the pr4-nanoflows-create-drop branch from db1a8b7 to 2356562 Compare April 23, 2026 15:51
@retran retran force-pushed the pr3-type-assertion-hardening branch from 231606c to fd6530f Compare April 23, 2026 19:42
@retran retran force-pushed the pr4-nanoflows-create-drop branch from 2356562 to 4588cda Compare April 23, 2026 19:42
@retran retran requested a review from Copilot April 23, 2026 19:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +14 to +17
// 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.
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +118
// 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"
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +55
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
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +159
// 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)
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
@retran retran force-pushed the pr3-type-assertion-hardening branch from fd6530f to a1e8dad Compare April 23, 2026 20:11
@retran retran force-pushed the pr4-nanoflows-create-drop branch from 4588cda to bc2a53a Compare April 23, 2026 20:11
@retran retran force-pushed the pr3-type-assertion-hardening branch from a1e8dad to fd9950e Compare April 23, 2026 20:28
@retran retran force-pushed the pr4-nanoflows-create-drop branch from bc2a53a to dabcdfb Compare April 23, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants