Skip to content

Commit 3575319

Browse files
akoclaude
andcommitted
fix: render microflow activity bodies in diff-local output
diff-local emitted a hardcoded `-- (microflow body)` stub for every microflow, so both sides of the diff were identical and the actual changes were invisible. Replace the stub with the same renderer used by DESCRIBE MICROFLOW. - Extract ParseMicroflowBSON / ParseMicroflowFromRaw as standalone functions so non-Reader callers (git-history BSON blobs) can parse microflows without an mpr.Reader. - Factor renderMicroflowMDL out of describeMicroflowToString as a shared emitter; DESCRIBE still populates the ELK source map, diff callers pass nil. - Wire diff-local's microflowBsonToMDL through the shared renderer with a name-lookup helper that resolves entity/microflow IDs against the working-tree model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f43a3f9 commit 3575319

3 files changed

Lines changed: 99 additions & 62 deletions

File tree

mdl/executor/cmd_diff_local.go

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/mendixlabs/mxcli/mdl/ast"
1414
"github.com/mendixlabs/mxcli/model"
15+
"github.com/mendixlabs/mxcli/sdk/mpr"
1516
"go.mongodb.org/mongo-driver/bson"
1617
"go.mongodb.org/mongo-driver/bson/primitive"
1718
)
@@ -484,55 +485,54 @@ func (e *Executor) attributeBsonToMDL(raw map[string]any) string {
484485
return result.String()
485486
}
486487

487-
// microflowBsonToMDL converts a microflow BSON to MDL
488+
// microflowBsonToMDL converts a microflow BSON to MDL using the same
489+
// renderer as DESCRIBE MICROFLOW, so diffs include activity bodies.
490+
// Falls back to a header-only stub if parsing fails.
488491
func (e *Executor) microflowBsonToMDL(raw map[string]any, qualifiedName string) string {
489-
var lines []string
492+
qn := splitQualifiedName(qualifiedName)
493+
mf := mpr.ParseMicroflowFromRaw(raw, model.ID(qn.Name), "")
490494

491-
// Documentation
492-
if doc := extractString(raw["Documentation"]); doc != "" {
493-
lines = append(lines, "/**")
494-
lines = append(lines, " * "+doc)
495-
lines = append(lines, " */")
495+
entityNames, microflowNames := e.buildNameLookups()
496+
return e.renderMicroflowMDL(mf, qn, entityNames, microflowNames, nil)
497+
}
498+
499+
// splitQualifiedName parses "Module.Name" into an ast.QualifiedName.
500+
// If no module prefix is present, Module is empty.
501+
func splitQualifiedName(qualifiedName string) ast.QualifiedName {
502+
if idx := strings.LastIndex(qualifiedName, "."); idx > 0 {
503+
return ast.QualifiedName{Module: qualifiedName[:idx], Name: qualifiedName[idx+1:]}
496504
}
505+
return ast.QualifiedName{Name: qualifiedName}
506+
}
497507

498-
lines = append(lines, fmt.Sprintf("CREATE MICROFLOW %s (", qualifiedName))
499-
500-
// Parameters - look in ObjectCollection
501-
if objCollection, ok := raw["ObjectCollection"].(map[string]any); ok {
502-
objects := extractBsonArray(objCollection["Objects"])
503-
for i, obj := range objects {
504-
if objMap, ok := obj.(map[string]any); ok {
505-
if objType := extractString(objMap["$Type"]); strings.Contains(objType, "MicroflowParameterObject") {
506-
paramName := extractString(objMap["Name"])
507-
paramType := "Object"
508-
if typeStr := extractString(objMap["VariableType"]); typeStr != "" {
509-
paramType = typeStr
510-
}
511-
comma := ","
512-
if i == len(objects)-1 {
513-
comma = ""
514-
}
515-
lines = append(lines, fmt.Sprintf(" $%s: %s%s", paramName, paramType, comma))
516-
}
508+
// buildNameLookups builds ID → qualified-name maps for entities and
509+
// microflows from the current project. Used by BSON-driven renderers that
510+
// receive IDs (e.g. entity references) and want to resolve them against
511+
// the working-tree model. Returns empty maps if the reader is unavailable.
512+
func (e *Executor) buildNameLookups() (map[model.ID]string, map[model.ID]string) {
513+
entityNames := make(map[model.ID]string)
514+
microflowNames := make(map[model.ID]string)
515+
if e.reader == nil {
516+
return entityNames, microflowNames
517+
}
518+
h, err := e.getHierarchy()
519+
if err != nil {
520+
return entityNames, microflowNames
521+
}
522+
if domainModels, err := e.reader.ListDomainModels(); err == nil {
523+
for _, dm := range domainModels {
524+
modName := h.GetModuleName(dm.ContainerID)
525+
for _, entity := range dm.Entities {
526+
entityNames[entity.ID] = modName + "." + entity.Name
517527
}
518528
}
519529
}
520-
521-
lines = append(lines, ")")
522-
523-
// Return type
524-
returnType := "Void"
525-
if rt := extractString(raw["ReturnType"]); rt != "" {
526-
returnType = rt
530+
if microflows, err := e.reader.ListMicroflows(); err == nil {
531+
for _, mf := range microflows {
532+
microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name)
533+
}
527534
}
528-
lines = append(lines, fmt.Sprintf("RETURNS %s", returnType))
529-
530-
lines = append(lines, "BEGIN")
531-
lines = append(lines, " -- (microflow body)")
532-
lines = append(lines, "END;")
533-
lines = append(lines, "/")
534-
535-
return strings.Join(lines, "\n")
535+
return entityNames, microflowNames
536536
}
537537

538538
// nanoflowBsonToMDL converts a nanoflow BSON to MDL

mdl/executor/cmd_microflows_show.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -458,26 +458,49 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma
458458
return "", nil, fmt.Errorf("microflow not found: %s", name)
459459
}
460460

461+
sourceMap := make(map[string]elkSourceRange)
462+
mdl := e.renderMicroflowMDL(targetMf, name, entityNames, microflowNames, sourceMap)
463+
return mdl, sourceMap, nil
464+
}
465+
466+
// renderMicroflowMDL formats a parsed Microflow as MDL text.
467+
//
468+
// Shared by DESCRIBE MICROFLOW and `diff-local`, so both paths produce the
469+
// same output. entityNames/microflowNames provide ID → qualified-name
470+
// resolution; pass empty maps if unavailable (types will fall back to
471+
// "Object"/"List" stubs). If sourceMap is non-nil it will be populated with
472+
// ELK node IDs → line ranges for visualization; pass nil when not needed.
473+
func (e *Executor) renderMicroflowMDL(
474+
mf *microflows.Microflow,
475+
name ast.QualifiedName,
476+
entityNames map[model.ID]string,
477+
microflowNames map[model.ID]string,
478+
sourceMap map[string]elkSourceRange,
479+
) string {
461480
var lines []string
462481

463-
if targetMf.Documentation != "" {
482+
if mf.Documentation != "" {
464483
lines = append(lines, "/**")
465-
for docLine := range strings.SplitSeq(targetMf.Documentation, "\n") {
484+
for docLine := range strings.SplitSeq(mf.Documentation, "\n") {
466485
lines = append(lines, " * "+docLine)
467486
}
468487
lines = append(lines, " */")
469488
}
470489

490+
if mf.Excluded {
491+
lines = append(lines, "@excluded")
492+
}
493+
471494
qualifiedName := name.Module + "." + name.Name
472-
if len(targetMf.Parameters) > 0 {
495+
if len(mf.Parameters) > 0 {
473496
lines = append(lines, fmt.Sprintf("CREATE OR MODIFY MICROFLOW %s (", qualifiedName))
474-
for i, param := range targetMf.Parameters {
497+
for i, param := range mf.Parameters {
475498
paramType := "Object"
476499
if param.Type != nil {
477500
paramType = e.formatMicroflowDataType(param.Type, entityNames)
478501
}
479502
comma := ","
480-
if i == len(targetMf.Parameters)-1 {
503+
if i == len(mf.Parameters)-1 {
481504
comma = ""
482505
}
483506
lines = append(lines, fmt.Sprintf(" $%s: %s%s", param.Name, paramType, comma))
@@ -487,24 +510,27 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma
487510
lines = append(lines, fmt.Sprintf("CREATE OR MODIFY MICROFLOW %s ()", qualifiedName))
488511
}
489512

490-
if targetMf.ReturnType != nil {
491-
returnType := e.formatMicroflowDataType(targetMf.ReturnType, entityNames)
513+
if mf.ReturnType != nil {
514+
returnType := e.formatMicroflowDataType(mf.ReturnType, entityNames)
492515
if returnType != "Void" && returnType != "" {
493516
returnLine := fmt.Sprintf("RETURNS %s", returnType)
494-
if targetMf.ReturnVariableName != "" && targetMf.ReturnVariableName != "Variable" {
495-
returnLine += fmt.Sprintf(" AS $%s", targetMf.ReturnVariableName)
517+
if mf.ReturnVariableName != "" && mf.ReturnVariableName != "Variable" {
518+
returnLine += fmt.Sprintf(" AS $%s", mf.ReturnVariableName)
496519
}
497520
lines = append(lines, returnLine)
498521
}
499522
}
500523

501524
lines = append(lines, "BEGIN")
502-
headerLineCount := len(lines) // lines before activity body
525+
headerLineCount := len(lines)
503526

504-
sourceMap := make(map[string]elkSourceRange)
505-
506-
if targetMf.ObjectCollection != nil && len(targetMf.ObjectCollection.Objects) > 0 {
507-
activityLines := e.formatMicroflowActivitiesWithSourceMap(targetMf, entityNames, microflowNames, sourceMap, headerLineCount)
527+
if mf.ObjectCollection != nil && len(mf.ObjectCollection.Objects) > 0 {
528+
var activityLines []string
529+
if sourceMap != nil {
530+
activityLines = e.formatMicroflowActivitiesWithSourceMap(mf, entityNames, microflowNames, sourceMap, headerLineCount)
531+
} else {
532+
activityLines = e.formatMicroflowActivities(mf, entityNames, microflowNames)
533+
}
508534
for _, line := range activityLines {
509535
lines = append(lines, " "+line)
510536
}
@@ -514,9 +540,9 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma
514540

515541
lines = append(lines, "END;")
516542

517-
if len(targetMf.AllowedModuleRoles) > 0 {
518-
roles := make([]string, len(targetMf.AllowedModuleRoles))
519-
for i, r := range targetMf.AllowedModuleRoles {
543+
if len(mf.AllowedModuleRoles) > 0 {
544+
roles := make([]string, len(mf.AllowedModuleRoles))
545+
for i, r := range mf.AllowedModuleRoles {
520546
roles[i] = string(r)
521547
}
522548
lines = append(lines, "")
@@ -526,7 +552,7 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma
526552

527553
lines = append(lines, "/")
528554

529-
return strings.Join(lines, "\n"), sourceMap, nil
555+
return strings.Join(lines, "\n")
530556
}
531557

532558
// formatMicroflowDataType formats a microflow data type for MDL output.

sdk/mpr/parser_microflow.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,27 @@ func (r *Reader) parseMicroflow(unitID, containerID string, contents []byte) (*m
1919
if err != nil {
2020
return nil, err
2121
}
22+
return ParseMicroflowBSON(contents, model.ID(unitID), model.ID(containerID))
23+
}
2224

25+
// ParseMicroflowBSON parses raw microflow BSON bytes into a Microflow.
26+
// Unlike (*Reader).parseMicroflow it does not require a Reader, so it can
27+
// parse arbitrary blobs (e.g. historical versions read via `git show`).
28+
func ParseMicroflowBSON(contents []byte, unitID, containerID model.ID) (*microflows.Microflow, error) {
2329
var raw map[string]any
2430
if err := bson.Unmarshal(contents, &raw); err != nil {
2531
return nil, fmt.Errorf("failed to unmarshal BSON: %w", err)
2632
}
33+
return ParseMicroflowFromRaw(raw, unitID, containerID), nil
34+
}
2735

36+
// ParseMicroflowFromRaw builds a Microflow from an already-unmarshalled BSON map.
37+
// Useful when the caller already has the decoded map (e.g. diff-local).
38+
func ParseMicroflowFromRaw(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow {
2839
mf := &microflows.Microflow{}
29-
mf.ID = model.ID(unitID)
40+
mf.ID = unitID
3041
mf.TypeName = "Microflows$Microflow"
31-
mf.ContainerID = model.ID(containerID)
42+
mf.ContainerID = containerID
3243

3344
if name, ok := raw["Name"].(string); ok {
3445
mf.Name = name
@@ -128,7 +139,7 @@ func (r *Reader) parseMicroflow(unitID, containerID string, contents []byte) (*m
128139
}
129140
}
130141

131-
return mf, nil
142+
return mf
132143
}
133144

134145
// parseSequenceFlow parses a SequenceFlow from raw BSON data.

0 commit comments

Comments
 (0)