Skip to content

Commit bc2a53a

Browse files
committed
feat: add CREATE/DROP NANOFLOW support — grammar, AST, visitor, executor
1 parent a1e8dad commit bc2a53a

15 files changed

Lines changed: 10807 additions & 9856 deletions

mdl/ast/ast_microflow.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ type DropMicroflowStmt struct {
6565

6666
func (s *DropMicroflowStmt) isStatement() {}
6767

68+
// CreateNanoflowStmt represents: CREATE NANOFLOW Module.Name (params) RETURNS type BEGIN body END
69+
type CreateNanoflowStmt struct {
70+
Name QualifiedName
71+
Parameters []MicroflowParam
72+
ReturnType *MicroflowReturnType
73+
Body []MicroflowStatement
74+
Documentation string
75+
Comment string
76+
Folder string // Folder path within module
77+
CreateOrModify bool
78+
Excluded bool // @excluded — document excluded from project
79+
}
80+
81+
func (s *CreateNanoflowStmt) isStatement() {}
82+
83+
// DropNanoflowStmt represents: DROP NANOFLOW Module.Name
84+
type DropNanoflowStmt struct {
85+
Name QualifiedName
86+
}
87+
88+
func (s *DropNanoflowStmt) isStatement() {}
89+
6890
// ============================================================================
6991
// Microflow Body Statements
7092
// ============================================================================
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Package executor - CREATE NANOFLOW command
4+
package executor
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"github.com/mendixlabs/mxcli/mdl/ast"
11+
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
12+
"github.com/mendixlabs/mxcli/mdl/types"
13+
"github.com/mendixlabs/mxcli/model"
14+
"github.com/mendixlabs/mxcli/sdk/microflows"
15+
)
16+
17+
// execCreateNanoflow handles CREATE NANOFLOW statements.
18+
func execCreateNanoflow(ctx *ExecContext, s *ast.CreateNanoflowStmt) error {
19+
if !ctx.ConnectedForWrite() {
20+
return mdlerrors.NewNotConnectedWrite()
21+
}
22+
23+
// Find or auto-create module
24+
module, err := findOrCreateModule(ctx, s.Name.Module)
25+
if err != nil {
26+
return err
27+
}
28+
29+
// Resolve folder if specified
30+
containerID := module.ID
31+
if s.Folder != "" {
32+
folderID, err := resolveFolder(ctx, module.ID, s.Folder)
33+
if err != nil {
34+
return mdlerrors.NewBackend("resolve folder "+s.Folder, err)
35+
}
36+
containerID = folderID
37+
}
38+
39+
// Check if nanoflow with same name already exists in this module
40+
var existingID model.ID
41+
var existingContainerID model.ID
42+
existingNanoflows, err := ctx.Backend.ListNanoflows()
43+
if err != nil {
44+
return mdlerrors.NewBackend("check existing nanoflows", err)
45+
}
46+
for _, existing := range existingNanoflows {
47+
if existing.Name == s.Name.Name && getModuleID(ctx, existing.ContainerID) == module.ID {
48+
if !s.CreateOrModify {
49+
return mdlerrors.NewAlreadyExistsMsg("nanoflow", s.Name.Module+"."+s.Name.Name, "nanoflow '"+s.Name.Module+"."+s.Name.Name+"' already exists (use create or replace to overwrite)")
50+
}
51+
existingID = existing.ID
52+
existingContainerID = existing.ContainerID
53+
break
54+
}
55+
}
56+
57+
// For CREATE OR REPLACE/MODIFY, reuse the existing ID to preserve references
58+
qualifiedName := s.Name.Module + "." + s.Name.Name
59+
nanoflowID := model.ID(types.GenerateID())
60+
if existingID != "" {
61+
nanoflowID = existingID
62+
if s.Folder == "" {
63+
containerID = existingContainerID
64+
}
65+
} else if dropped := consumeDroppedNanoflow(ctx, qualifiedName); dropped != nil {
66+
nanoflowID = dropped.ID
67+
if s.Folder == "" && dropped.ContainerID != "" {
68+
containerID = dropped.ContainerID
69+
}
70+
}
71+
72+
// Build the nanoflow
73+
nf := &microflows.Nanoflow{
74+
BaseElement: model.BaseElement{
75+
ID: nanoflowID,
76+
},
77+
ContainerID: containerID,
78+
Name: s.Name.Name,
79+
Documentation: s.Documentation,
80+
MarkAsUsed: false,
81+
Excluded: s.Excluded,
82+
}
83+
84+
// Build entity resolver function for parameter/return types
85+
entityResolver := func(qn ast.QualifiedName) model.ID {
86+
dms, err := ctx.Backend.ListDomainModels()
87+
if err != nil {
88+
return ""
89+
}
90+
modules, _ := ctx.Backend.ListModules()
91+
moduleNames := make(map[model.ID]string)
92+
for _, m := range modules {
93+
moduleNames[m.ID] = m.Name
94+
}
95+
for _, dm := range dms {
96+
modName := moduleNames[dm.ContainerID]
97+
if modName != qn.Module {
98+
continue
99+
}
100+
for _, ent := range dm.Entities {
101+
if ent.Name == qn.Name {
102+
return ent.ID
103+
}
104+
}
105+
}
106+
return ""
107+
}
108+
109+
// Validate and add parameters
110+
for _, p := range s.Parameters {
111+
if p.Type.EntityRef != nil && !isBuiltinModuleEntity(p.Type.EntityRef.Module) {
112+
entityID := entityResolver(*p.Type.EntityRef)
113+
if entityID == "" {
114+
return mdlerrors.NewNotFoundMsg("entity", p.Type.EntityRef.Module+"."+p.Type.EntityRef.Name,
115+
fmt.Sprintf("entity '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name))
116+
}
117+
}
118+
if p.Type.Kind == ast.TypeEnumeration && p.Type.EnumRef != nil {
119+
if found := findEnumeration(ctx, p.Type.EnumRef.Module, p.Type.EnumRef.Name); found == nil {
120+
return mdlerrors.NewNotFoundMsg("enumeration", p.Type.EnumRef.Module+"."+p.Type.EnumRef.Name,
121+
fmt.Sprintf("enumeration '%s.%s' not found for parameter '%s'", p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name))
122+
}
123+
}
124+
param := &microflows.MicroflowParameter{
125+
BaseElement: model.BaseElement{
126+
ID: model.ID(types.GenerateID()),
127+
},
128+
ContainerID: nf.ID,
129+
Name: p.Name,
130+
Type: convertASTToMicroflowDataType(p.Type, entityResolver),
131+
}
132+
nf.Parameters = append(nf.Parameters, param)
133+
}
134+
135+
// Validate and set return type
136+
if s.ReturnType != nil {
137+
if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) {
138+
entityID := entityResolver(*s.ReturnType.Type.EntityRef)
139+
if entityID == "" {
140+
return mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name,
141+
fmt.Sprintf("entity '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name))
142+
}
143+
}
144+
if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil {
145+
if found := findEnumeration(ctx, s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name); found == nil {
146+
return mdlerrors.NewNotFoundMsg("enumeration", s.ReturnType.Type.EnumRef.Module+"."+s.ReturnType.Type.EnumRef.Name,
147+
fmt.Sprintf("enumeration '%s.%s' not found for return type", s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name))
148+
}
149+
}
150+
nf.ReturnType = convertASTToMicroflowDataType(s.ReturnType.Type, entityResolver)
151+
} else {
152+
nf.ReturnType = &microflows.VoidType{}
153+
}
154+
155+
// Validate nanoflow-specific constraints before building the flow graph
156+
qualName := s.Name.Module + "." + s.Name.Name
157+
if errMsg := validateNanoflow(qualName, s.Body, s.ReturnType); errMsg != "" {
158+
return fmt.Errorf("%s", errMsg)
159+
}
160+
161+
// Build flow graph from body statements
162+
varTypes := make(map[string]string)
163+
declaredVars := make(map[string]string)
164+
165+
for _, p := range s.Parameters {
166+
if p.Type.EntityRef != nil {
167+
entityQN := p.Type.EntityRef.Module + "." + p.Type.EntityRef.Name
168+
if p.Type.Kind == ast.TypeListOf {
169+
varTypes[p.Name] = "List of " + entityQN
170+
} else {
171+
varTypes[p.Name] = entityQN
172+
}
173+
} else {
174+
declaredVars[p.Name] = p.Type.Kind.String()
175+
}
176+
}
177+
178+
hierarchy, _ := getHierarchy(ctx)
179+
restServices, _ := loadRestServices(ctx)
180+
181+
builder := &flowBuilder{
182+
posX: 200,
183+
posY: 200,
184+
baseY: 200,
185+
spacing: HorizontalSpacing,
186+
varTypes: varTypes,
187+
declaredVars: declaredVars,
188+
measurer: &layoutMeasurer{varTypes: varTypes},
189+
backend: ctx.Backend,
190+
hierarchy: hierarchy,
191+
restServices: restServices,
192+
}
193+
194+
nf.ObjectCollection = builder.buildFlowGraph(s.Body, s.ReturnType)
195+
196+
// Check for validation errors
197+
if errors := builder.GetErrors(); len(errors) > 0 {
198+
var errMsg strings.Builder
199+
errMsg.WriteString(fmt.Sprintf("nanoflow '%s.%s' has validation errors:\n", s.Name.Module, s.Name.Name))
200+
for _, err := range errors {
201+
errMsg.WriteString(fmt.Sprintf(" - %s\n", err))
202+
}
203+
return fmt.Errorf("%s", errMsg.String())
204+
}
205+
206+
// Create or update the nanoflow
207+
if existingID != "" {
208+
if err := ctx.Backend.UpdateNanoflow(nf); err != nil {
209+
return mdlerrors.NewBackend("update nanoflow", err)
210+
}
211+
fmt.Fprintf(ctx.Output, "Replaced nanoflow: %s.%s\n", s.Name.Module, s.Name.Name)
212+
} else {
213+
if err := ctx.Backend.CreateNanoflow(nf); err != nil {
214+
return mdlerrors.NewBackend("create nanoflow", err)
215+
}
216+
fmt.Fprintf(ctx.Output, "Created nanoflow: %s.%s\n", s.Name.Module, s.Name.Name)
217+
}
218+
219+
// Track the created nanoflow
220+
returnEntityName := extractEntityFromReturnType(nf.ReturnType)
221+
ctx.trackCreatedNanoflow(s.Name.Module, s.Name.Name, nf.ID, containerID, returnEntityName)
222+
223+
invalidateHierarchy(ctx)
224+
return nil
225+
}

mdl/executor/cmd_nanoflows_drop.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Package executor - DROP NANOFLOW command
4+
package executor
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/mendixlabs/mxcli/mdl/ast"
10+
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
11+
)
12+
13+
// execDropNanoflow handles DROP NANOFLOW statements.
14+
func execDropNanoflow(ctx *ExecContext, s *ast.DropNanoflowStmt) error {
15+
if !ctx.ConnectedForWrite() {
16+
return mdlerrors.NewNotConnectedWrite()
17+
}
18+
19+
// Get hierarchy for module/folder resolution
20+
h, err := getHierarchy(ctx)
21+
if err != nil {
22+
return mdlerrors.NewBackend("build hierarchy", err)
23+
}
24+
25+
// Find and delete the nanoflow
26+
nfs, err := ctx.Backend.ListNanoflows()
27+
if err != nil {
28+
return mdlerrors.NewBackend("list nanoflows", err)
29+
}
30+
31+
for _, nf := range nfs {
32+
modID := h.FindModuleID(nf.ContainerID)
33+
modName := h.GetModuleName(modID)
34+
if modName == s.Name.Module && nf.Name == s.Name.Name {
35+
qualifiedName := s.Name.Module + "." + s.Name.Name
36+
rememberDroppedNanoflow(ctx, qualifiedName, nf.ID, nf.ContainerID)
37+
if err := ctx.Backend.DeleteNanoflow(nf.ID); err != nil {
38+
return mdlerrors.NewBackend("delete nanoflow", err)
39+
}
40+
// Clear executor-level caches
41+
if ctx.Cache != nil && ctx.Cache.createdNanoflows != nil {
42+
delete(ctx.Cache.createdNanoflows, qualifiedName)
43+
}
44+
invalidateHierarchy(ctx)
45+
fmt.Fprintf(ctx.Output, "Dropped nanoflow: %s.%s\n", s.Name.Module, s.Name.Name)
46+
return nil
47+
}
48+
}
49+
50+
return mdlerrors.NewNotFound("nanoflow", s.Name.Module+"."+s.Name.Name)
51+
}

mdl/executor/exec_context.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,22 @@ func (ctx *ExecContext) trackCreatedMicroflow(moduleName, mfName string, id, con
159159
}
160160
}
161161

162+
// trackCreatedNanoflow registers a nanoflow created during this session.
163+
func (ctx *ExecContext) trackCreatedNanoflow(moduleName, nfName string, id, containerID model.ID, returnEntityName string) {
164+
ctx.ensureCache()
165+
if ctx.Cache.createdNanoflows == nil {
166+
ctx.Cache.createdNanoflows = make(map[string]*createdNanoflowInfo)
167+
}
168+
qualifiedName := moduleName + "." + nfName
169+
ctx.Cache.createdNanoflows[qualifiedName] = &createdNanoflowInfo{
170+
ID: id,
171+
Name: nfName,
172+
ModuleName: moduleName,
173+
ContainerID: containerID,
174+
ReturnEntityName: returnEntityName,
175+
}
176+
}
177+
162178
// trackCreatedPage registers a page created during this session.
163179
func (ctx *ExecContext) trackCreatedPage(moduleName, pageName string, id, containerID model.ID) {
164180
ctx.ensureCache()

mdl/executor/executor.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type executorCache struct {
3232

3333
// Track items created during this session (not yet visible via reader)
3434
createdMicroflows map[string]*createdMicroflowInfo // qualifiedName -> info
35+
createdNanoflows map[string]*createdNanoflowInfo // qualifiedName -> info
3536
createdPages map[string]*createdPageInfo // qualifiedName -> info
3637
createdSnippets map[string]*createdSnippetInfo // qualifiedName -> info
3738

@@ -43,6 +44,7 @@ type executorCache struct {
4344
// rewrites. Reusing both keeps the rewrite semantically equivalent to an
4445
// in-place update.
4546
droppedMicroflows map[string]*droppedUnitInfo // qualifiedName -> original IDs
47+
droppedNanoflows map[string]*droppedUnitInfo // qualifiedName -> original IDs
4648

4749
// Track domain models modified during this session for finalization
4850
modifiedDomainModels map[model.ID]string // domain model unit ID -> module name
@@ -62,6 +64,15 @@ type createdMicroflowInfo struct {
6264
ReturnEntityName string // Qualified entity name from return type (e.g., "Module.Entity")
6365
}
6466

67+
// createdNanoflowInfo tracks a nanoflow created during this session.
68+
type createdNanoflowInfo struct {
69+
ID model.ID
70+
Name string
71+
ModuleName string
72+
ContainerID model.ID
73+
ReturnEntityName string
74+
}
75+
6576
// createdPageInfo tracks a page created during this session.
6677
type createdPageInfo struct {
6778
ID model.ID
@@ -373,3 +384,35 @@ func consumeDroppedMicroflow(ctx *ExecContext, qualifiedName string) *droppedUni
373384
delete(ctx.Cache.droppedMicroflows, qualifiedName)
374385
return info
375386
}
387+
388+
// rememberDroppedNanoflow records the UnitID and ContainerID of a nanoflow
389+
// that was just deleted so a subsequent CREATE OR REPLACE/MODIFY can reuse them.
390+
func rememberDroppedNanoflow(ctx *ExecContext, qualifiedName string, id, containerID model.ID) {
391+
if ctx == nil || qualifiedName == "" || id == "" {
392+
return
393+
}
394+
if ctx.Cache == nil {
395+
ctx.Cache = &executorCache{}
396+
}
397+
if ctx.Cache.droppedNanoflows == nil {
398+
ctx.Cache.droppedNanoflows = make(map[string]*droppedUnitInfo)
399+
}
400+
ctx.Cache.droppedNanoflows[qualifiedName] = &droppedUnitInfo{
401+
ID: id,
402+
ContainerID: containerID,
403+
}
404+
}
405+
406+
// consumeDroppedNanoflow returns the original IDs of a nanoflow dropped
407+
// earlier in this session (if any) and removes the entry.
408+
func consumeDroppedNanoflow(ctx *ExecContext, qualifiedName string) *droppedUnitInfo {
409+
if ctx == nil || ctx.Cache == nil || ctx.Cache.droppedNanoflows == nil {
410+
return nil
411+
}
412+
info, ok := ctx.Cache.droppedNanoflows[qualifiedName]
413+
if !ok {
414+
return nil
415+
}
416+
delete(ctx.Cache.droppedNanoflows, qualifiedName)
417+
return info
418+
}

0 commit comments

Comments
 (0)