|
9 | 9 |
|
10 | 10 | "github.com/mendixlabs/mxcli/mdl/ast" |
11 | 11 | "github.com/mendixlabs/mxcli/model" |
| 12 | + "github.com/mendixlabs/mxcli/sdk/domainmodel" |
12 | 13 | "github.com/mendixlabs/mxcli/sdk/microflows" |
13 | 14 | "github.com/mendixlabs/mxcli/sdk/mpr" |
14 | 15 | ) |
@@ -292,14 +293,51 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { |
292 | 293 | if s.StartVariable != "" { |
293 | 294 | // Association retrieve: RETRIEVE $List FROM $Parent/Module.AssocName |
294 | 295 | assocQN := s.Source.Module + "." + s.Source.Name |
295 | | - source = µflows.AssociationRetrieveSource{ |
296 | | - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, |
297 | | - StartVariable: s.StartVariable, |
298 | | - AssociationQualifiedName: assocQN, |
299 | | - } |
300 | | - // Association retrieve always returns a list |
| 296 | + |
| 297 | + // Look up association to determine type and direction. |
| 298 | + // For Reference associations, AssociationRetrieveSource always returns a single |
| 299 | + // object (the entity on the other end). When the user navigates from the child |
| 300 | + // (non-owner) side, the intent is to get a list of parent entities — we must use |
| 301 | + // a DatabaseRetrieveSource with XPath constraint instead. |
| 302 | + assocInfo := fb.lookupAssociation(s.Source.Module, s.Source.Name) |
| 303 | + startVarType := "" |
301 | 304 | if fb.varTypes != nil { |
302 | | - fb.varTypes[s.Variable] = "List of " + assocQN |
| 305 | + startVarType = fb.varTypes[s.StartVariable] |
| 306 | + } |
| 307 | + |
| 308 | + if assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference && |
| 309 | + assocInfo.childEntityQN != "" && startVarType == assocInfo.childEntityQN { |
| 310 | + // Reverse traversal on Reference: child → parent (one-to-many) |
| 311 | + // Use DatabaseRetrieveSource with XPath to get a list of parent entities |
| 312 | + dbSource := µflows.DatabaseRetrieveSource{ |
| 313 | + BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, |
| 314 | + EntityQualifiedName: assocInfo.parentEntityQN, |
| 315 | + XPathConstraint: "[" + assocQN + " = $" + s.StartVariable + "]", |
| 316 | + } |
| 317 | + source = dbSource |
| 318 | + if fb.varTypes != nil { |
| 319 | + fb.varTypes[s.Variable] = "List of " + assocInfo.parentEntityQN |
| 320 | + } |
| 321 | + } else { |
| 322 | + // Forward traversal or ReferenceSet: use AssociationRetrieveSource |
| 323 | + source = µflows.AssociationRetrieveSource{ |
| 324 | + BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, |
| 325 | + StartVariable: s.StartVariable, |
| 326 | + AssociationQualifiedName: assocQN, |
| 327 | + } |
| 328 | + if fb.varTypes != nil { |
| 329 | + if assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference { |
| 330 | + // Reference forward traversal: returns single object |
| 331 | + otherEntity := assocInfo.childEntityQN |
| 332 | + if startVarType == assocInfo.childEntityQN { |
| 333 | + otherEntity = assocInfo.parentEntityQN |
| 334 | + } |
| 335 | + fb.varTypes[s.Variable] = otherEntity |
| 336 | + } else { |
| 337 | + // ReferenceSet or unknown: returns a list |
| 338 | + fb.varTypes[s.Variable] = "List of " + assocQN |
| 339 | + } |
| 340 | + } |
303 | 341 | } |
304 | 342 | } else { |
305 | 343 | // Database retrieve: RETRIEVE $List FROM Module.Entity WHERE ... |
@@ -687,3 +725,44 @@ func (fb *flowBuilder) addRemoveFromListAction(s *ast.RemoveFromListStmt) model. |
687 | 725 | fb.posX += fb.spacing |
688 | 726 | return activity.ID |
689 | 727 | } |
| 728 | + |
| 729 | +// assocLookupResult holds resolved association metadata. |
| 730 | +type assocLookupResult struct { |
| 731 | + Type domainmodel.AssociationType |
| 732 | + parentEntityQN string // Qualified name of the parent (FROM/owner) entity |
| 733 | + childEntityQN string // Qualified name of the child (TO/referenced) entity |
| 734 | +} |
| 735 | + |
| 736 | +// lookupAssociation finds an association by module and name, returning its type |
| 737 | +// and the qualified names of its parent and child entities. Returns nil if the |
| 738 | +// association cannot be found (e.g., reader is nil or module doesn't exist). |
| 739 | +func (fb *flowBuilder) lookupAssociation(moduleName, assocName string) *assocLookupResult { |
| 740 | + if fb.reader == nil { |
| 741 | + return nil |
| 742 | + } |
| 743 | + mod, err := fb.reader.GetModuleByName(moduleName) |
| 744 | + if err != nil || mod == nil { |
| 745 | + return nil |
| 746 | + } |
| 747 | + dm, err := fb.reader.GetDomainModel(mod.ID) |
| 748 | + if err != nil || dm == nil { |
| 749 | + return nil |
| 750 | + } |
| 751 | + |
| 752 | + // Build entity ID → qualified name map |
| 753 | + entityNames := make(map[model.ID]string, len(dm.Entities)) |
| 754 | + for _, e := range dm.Entities { |
| 755 | + entityNames[e.ID] = moduleName + "." + e.Name |
| 756 | + } |
| 757 | + |
| 758 | + for _, a := range dm.Associations { |
| 759 | + if a.Name == assocName { |
| 760 | + return &assocLookupResult{ |
| 761 | + Type: a.Type, |
| 762 | + parentEntityQN: entityNames[a.ParentID], |
| 763 | + childEntityQN: entityNames[a.ChildID], |
| 764 | + } |
| 765 | + } |
| 766 | + } |
| 767 | + return nil |
| 768 | +} |
0 commit comments