From fdbc43aab5d1e2f3b30bd0ebfb00422f12b280ae Mon Sep 17 00:00:00 2001 From: Victor Soria Date: Thu, 7 May 2026 10:54:34 -0500 Subject: [PATCH] fix(relationships): normalize source_element/target_element to source_id at write time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP and REST API were storing element UUIDs in source_element/target_element. All 11 viewer queries (Integration Map, App Landscape, Dependency Graph, Capability Tree, Technology Stack, etc.) JOIN on elements.source_id, so they returned empty results for MCP-created workspaces. Fix: resolve UUID→source_id in the relationship create/update handlers before persisting. AOEF-imported values (already source_ids) pass through unchanged. Migration 025 backfills existing rows that contain UUIDs. --- internal/api/relationship_handler.go | 24 ++++++++++++++++++- ...25_normalize_relationship_element_refs.sql | 22 +++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 migrations/025_normalize_relationship_element_refs.sql diff --git a/internal/api/relationship_handler.go b/internal/api/relationship_handler.go index f10ae55..13e9d90 100644 --- a/internal/api/relationship_handler.go +++ b/internal/api/relationship_handler.go @@ -29,6 +29,24 @@ type relResponse struct { Violations []parser.RelationshipViolation `json:"violations,omitempty"` } +// resolveElementRef translates a UUID element reference to its ArchiMate source_id. +// If ref is already a source_id (not a valid UUID) it is returned unchanged. +// Falls back to ref itself when the UUID is not found in the workspace. +func resolveElementRef(db *sql.DB, workspaceID uuid.UUID, ref string) string { + id, err := uuid.Parse(ref) + if err != nil { + return ref + } + var sourceID string + if err := db.QueryRow( + `SELECT source_id FROM elements WHERE id = $1 AND workspace_id = $2`, + id, workspaceID, + ).Scan(&sourceID); err != nil || sourceID == "" { + return ref + } + return sourceID +} + // elementTypesBySourceIDs returns a source_id→type map for the given IDs in a workspace. func (h *relationshipHandler) elementTypesBySourceIDs(wsID uuid.UUID, sourceIDs []string) (map[string]string, error) { rows, err := h.db.Query( @@ -115,6 +133,8 @@ func (h *relationshipHandler) create(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, errorf("source_id, type, source_element and target_element are required")) return } + body.SourceElement = resolveElementRef(h.db, wsID, body.SourceElement) + body.TargetElement = resolveElementRef(h.db, wsID, body.TargetElement) rel, err := h.store.Create(wsID, body.SourceID, body.Type, body.SourceElement, body.TargetElement, body.Name, body.Documentation) if err != nil { respondError(w, http.StatusInternalServerError, err) @@ -151,6 +171,9 @@ func (h *relationshipHandler) update(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, err) return } + wsID, _ := uuid.Parse(chi.URLParam(r, "wsID")) + body.SourceElement = resolveElementRef(h.db, wsID, body.SourceElement) + body.TargetElement = resolveElementRef(h.db, wsID, body.TargetElement) rel, err := h.store.Update(id, body.Type, body.SourceElement, body.TargetElement, body.Name, body.Documentation, body.Version) if errors.Is(err, relationship.ErrNotFound) { respondError(w, http.StatusNotFound, err) @@ -164,7 +187,6 @@ func (h *relationshipHandler) update(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusInternalServerError, err) return } - wsID, _ := uuid.Parse(chi.URLParam(r, "wsID")) if claims := auth.ClaimsFromCtx(r.Context()); claims != nil && h.audit != nil { _ = h.audit.Record(audit.RecordParams{ WorkspaceID: wsID, UserID: claims.UserID, UserEmail: claims.Email, diff --git a/migrations/025_normalize_relationship_element_refs.sql b/migrations/025_normalize_relationship_element_refs.sql new file mode 100644 index 0000000..933f55c --- /dev/null +++ b/migrations/025_normalize_relationship_element_refs.sql @@ -0,0 +1,22 @@ +-- Normalize source_element / target_element for relationships created via the +-- MCP server or REST API before the resolveElementRef fix. Those paths stored +-- element UUIDs instead of ArchiMate source_ids, breaking all viewer queries +-- that JOIN on elements.source_id = relationships.source_element/target_element. + +UPDATE relationships r +SET source_element = COALESCE( + (SELECT e.source_id FROM elements e + WHERE e.id::text = r.source_element + AND e.workspace_id = r.workspace_id + AND e.source_id <> ''), + r.source_element) +WHERE r.source_element ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; + +UPDATE relationships r +SET target_element = COALESCE( + (SELECT e.source_id FROM elements e + WHERE e.id::text = r.target_element + AND e.workspace_id = r.workspace_id + AND e.source_id <> ''), + r.target_element) +WHERE r.target_element ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';