From 09bb18cedabb7e9f11c4d8d0dde95c5acaf968fd Mon Sep 17 00:00:00 2001 From: Maksym Dolina Date: Fri, 8 May 2026 21:03:20 +0100 Subject: [PATCH 1/4] Add tag value API endpoints and related functionality - Created endpoints to add tag value and retrieve tag value. --- common/const_var.go | 1 + taggingapi/router.go | 2 ++ taggingapi/tag/tag_handler.go | 27 +++++++++++++++++++++++++++ taggingapi/tag/tag_member_handler.go | 27 +++++++++++++++++++++------ taggingapi/tag/tag_member_service.go | 16 ++++++++-------- taggingapi/tag/tag_service.go | 25 +++++++++++++++++++++++-- 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/common/const_var.go b/common/const_var.go index cdd6cae..30378ce 100644 --- a/common/const_var.go +++ b/common/const_var.go @@ -184,6 +184,7 @@ const ( const ( Member = "member" Tag = "tag" + TagValue = "value" StartRange = "startRange" EndRange = "endRange" ) diff --git a/taggingapi/router.go b/taggingapi/router.go index ae5b66b..4aa33fa 100644 --- a/taggingapi/router.go +++ b/taggingapi/router.go @@ -28,6 +28,7 @@ func routeTaggingServiceApis(r *mux.Router, s *xhttp.WebconfigServer) { taggingPath.HandleFunc("", tag.GetAllTagsHandler).Methods("GET").Name("Get-all-tags") taggingPath.HandleFunc("/{tag}", tag.GetTagByIdHandler).Methods("GET").Name("Get-tag-by-id") taggingPath.HandleFunc("/{tag}/members", tag.AddMembersToTagHandler).Methods("PUT").Name("Add-members-to-tag") + taggingPath.HandleFunc("/{tag}/values/{value}/members", tag.AddMembersToTagHandler).Methods("PUT").Name("Add-members-to-tag-with-value") taggingPath.HandleFunc("/{tag}", tag.DeleteTagHandler).Methods("DELETE").Name("Delete-tag-v2") taggingPath.HandleFunc("/{tag}/members", tag.RemoveMembersFromTagHandler).Methods("DELETE").Name("Remove-members-from-tag") taggingPath.HandleFunc("/{tag}/members/{member}", tag.RemoveMemberFromTagHandler).Methods("DELETE").Name("Remove-member-from-tag") @@ -35,6 +36,7 @@ func routeTaggingServiceApis(r *mux.Router, s *xhttp.WebconfigServer) { taggingPath.HandleFunc("/{tag}/members", tag.GetTagMembersHandler).Methods("GET").Name("Get-tag-members") taggingPath.HandleFunc("/members/{member}", tag.GetTagsByMemberHandler).Methods("GET").Name("Get-tags-by-member") + taggingPath.HandleFunc("/members/{member}/values", tag.GetTagsWithValuesByMemberHandler).Methods("GET").Name("Get-tags-with-values-by-member") paths = append(paths, taggingPath) diff --git a/taggingapi/tag/tag_handler.go b/taggingapi/tag/tag_handler.go index 372c107..0511a77 100644 --- a/taggingapi/tag/tag_handler.go +++ b/taggingapi/tag/tag_handler.go @@ -27,7 +27,34 @@ func GetTagsByMemberHandler(w http.ResponseWriter, r *http.Request) { xhttp.WriteXconfResponse(w, http.StatusBadRequest, []byte(fmt.Sprintf(NotSpecifiedErrorMsg, common.Member))) return } + tags, err := GetTagsByMember(member) + if err != nil { + xhttp.WriteXconfErrorResponse(w, err) + return + } + + respBytes, err := json.Marshal(tags) + if err != nil { + xhttp.WriteXconfErrorResponse(w, err) + return + } + xhttp.WriteXconfResponse(w, http.StatusOK, respBytes) +} + +func GetTagsWithValuesByMemberHandler(w http.ResponseWriter, r *http.Request) { + member, found := mux.Vars(r)[common.Member] + if !found { + xhttp.WriteXconfResponse(w, http.StatusBadRequest, []byte(fmt.Sprintf(NotSpecifiedErrorMsg, common.Member))) + return + } + + tags, err := GetTagsWithValuesByMember(member) + if err != nil { + xhttp.WriteXconfErrorResponse(w, err) + return + } + respBytes, err := json.Marshal(tags) if err != nil { xhttp.WriteXconfErrorResponse(w, err) diff --git a/taggingapi/tag/tag_member_handler.go b/taggingapi/tag/tag_member_handler.go index 55b3cd2..853ee86 100644 --- a/taggingapi/tag/tag_member_handler.go +++ b/taggingapi/tag/tag_member_handler.go @@ -111,6 +111,8 @@ func AddMembersToTagHandler(w http.ResponseWriter, r *http.Request) { return } + tagValue := mux.Vars(r)[common.TagValue] // "" when called via /{tag}/members (old route) + xw, ok := w.(*xwhttp.XResponseWriter) if !ok { xhttp.WriteXconfResponse(w, http.StatusInternalServerError, []byte(ResponseWriterCastErrorMsg)) @@ -134,9 +136,7 @@ func AddMembersToTagHandler(w http.ResponseWriter, r *http.Request) { return } - log.Debugf("AddMembers request: tag=%s, memberCount=%d", tagId, len(members)) - - stored, err := AddMembersWithXdas(tagId, members) + stored, err := AddMembersWithXdas(tagId, members, tagValue) if err != nil { xhttp.WriteXconfErrorResponse(w, err) return @@ -186,8 +186,6 @@ func RemoveMembersFromTagHandler(w http.ResponseWriter, r *http.Request) { return } - log.Debugf("RemoveMembers request: tag=%s, memberCount=%d", id, len(members)) - removed, err := RemoveMembersWithXdas(id, members) if err != nil { xhttp.WriteXconfErrorResponse(w, err) @@ -292,6 +290,12 @@ func GetTagByIdHandler(w http.ResponseWriter, r *http.Request) { // DeleteTagHandler deletes a tag and all its members from V2 storage asynchronously func DeleteTagHandler(w http.ResponseWriter, r *http.Request) { + xw, ok := w.(*xwhttp.XResponseWriter) + if !ok { + xhttp.WriteXconfResponse(w, http.StatusInternalServerError, []byte(ResponseWriterCastErrorMsg)) + return + } + id, found := mux.Vars(r)[common.Tag] if !found { xhttp.WriteXconfResponse(w, http.StatusBadRequest, []byte(fmt.Sprintf(NotSpecifiedErrorMsg, common.Tag))) @@ -309,9 +313,20 @@ func DeleteTagHandler(w http.ResponseWriter, r *http.Request) { return } + auditId := xw.AuditId() go func(tagId string) { if err := DeleteTag(tagId); err != nil { - log.Errorf("Background deletion failed for tag '%s': %v", tagId, err) + log.WithFields(log.Fields{ + "audit_id": auditId, + "endpoint": "DeleteTag", + "tag": tagId, + }).Errorf("background deletion failed: %v", err) + } else { + log.WithFields(log.Fields{ + "audit_id": auditId, + "endpoint": "DeleteTag", + "tag": tagId, + }).Info("tagging background deletion completed") } }(id) diff --git a/taggingapi/tag/tag_member_service.go b/taggingapi/tag/tag_member_service.go index bd6884a..169a0a5 100644 --- a/taggingapi/tag/tag_member_service.go +++ b/taggingapi/tag/tag_member_service.go @@ -240,7 +240,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb limit = DefaultPageSizeV2 } - log.Debugf("Getting paginated members for tag %s, limit %d, cursor %s", tagId, limit, cursor) + log.Infof("Getting paginated members for tag %s, limit %d, cursor %s", tagId, limit, cursor) populatedBuckets, err := getPopulatedBuckets(tagId) if err != nil { @@ -251,7 +251,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb return nil, xwcommon.NewRemoteErrorAS(http.StatusNotFound, fmt.Sprintf(NotFoundErrorMsg, tagId)) } - log.Debugf("Found %d populated buckets for tag %s", len(populatedBuckets), tagId) + log.Infof("Found %d populated buckets for tag %s", len(populatedBuckets), tagId) state := parseBucketedCursor(cursor) var allMembers []string @@ -297,7 +297,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb if len(result.members) > needed { allMembers = append(allMembers, result.members[:needed]...) nextCursor := generateBucketedCursor(currentBucketId, result.members[needed-1], len(allMembers)) - log.Debugf("Returning %d members for tag %s with more data in bucket %d", + log.Infof("Returning %d members for tag %s with more data in bucket %d", len(allMembers), tagId, currentBucketId) return &PaginatedMembersResponse{ Data: allMembers, @@ -322,7 +322,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb nextCursor = generateBucketedCursor(nextBucketId, "", 0) } - log.Debugf("Returning %d members for tag %s, hasMore: %v", len(allMembers), tagId, hasMore) + log.Infof("Returning %d members for tag %s, hasMore: %v", len(allMembers), tagId, hasMore) return &PaginatedMembersResponse{ Data: allMembers, NextCursor: nextCursor, @@ -553,7 +553,7 @@ func fetchMembersFromBucketsConcurrent(tagId string, bucketIds []int, totalLimit // AddMembersWithXdas adds members to both XDAS and Cassandra (XDAS-first approach) // Returns the count of members actually stored to Cassandra. -func AddMembersWithXdas(tagId string, members []string) (int, error) { +func AddMembersWithXdas(tagId string, members []string, tagValue string) (int, error) { startTime := time.Now() if len(members) == 0 { @@ -564,7 +564,7 @@ func AddMembersWithXdas(tagId string, members []string) (int, error) { return 0, fmt.Errorf("batch size %d exceeds maximum %d", len(members), MaxBatchSizeV2) } - savedToXdasMembers, err := addMembersToXdas(tagId, members) + savedToXdasMembers, err := addMembersToXdas(tagId, members, tagValue) if err != nil { return 0, fmt.Errorf("XDAS operation failed: %w", err) } @@ -630,7 +630,7 @@ func RemoveMemberWithXdas(tagId string, member string) error { } // addMembersToXdas adds members to Xdas using concurrent workers (similar to V1 pattern) -func addMembersToXdas(tagId string, members []string) ([]string, error) { +func addMembersToXdas(tagId string, members []string, tagValue string) ([]string, error) { tagId = SetTagPrefix(tagId) membersChannel := make(chan string, len(members)) @@ -653,7 +653,7 @@ func addMembersToXdas(tagId string, members []string) ([]string, error) { } for i := 0; i < numOfWorkers; i++ { wg.Add(1) - go storeTagMembersInXdas(tagId, membersChannel, savedMembersChannel, wg) + go storeTagMembersInXdas(tagId, membersChannel, savedMembersChannel, wg, tagValue) } go func() { diff --git a/taggingapi/tag/tag_service.go b/taggingapi/tag/tag_service.go index b636fba..273596f 100644 --- a/taggingapi/tag/tag_service.go +++ b/taggingapi/tag/tag_service.go @@ -39,6 +39,17 @@ func GetTagsByMember(member string) ([]string, error) { return filterTagEntriesByPrefix(tagsMap.Keys()), err } +func GetTagsWithValuesByMember(member string) (map[string]string, error) { + member = ToNormalizedEcm(member) + tagsAsHashes, err := GetGroupServiceConnector().GetGroupsMemberBelongsTo(member) + if err != nil { + log.Errorf("xdas error getting members by %s group: %s", member, err.Error()) + return map[string]string{}, err + } + tagsMap := util.StringMap(tagsAsHashes.GetFields()) + return filterTagEntriesWithValuesByPrefix(tagsMap), err +} + func filterTagEntriesByPrefix(ftEntries []string) []string { tags := []string{} for _, ftEntry := range ftEntries { @@ -49,10 +60,20 @@ func filterTagEntriesByPrefix(ftEntries []string) []string { return tags } -func storeTagMembersInXdas(id string, members <-chan string, savedMembers chan<- string, wg *sync.WaitGroup) { +func filterTagEntriesWithValuesByPrefix(entries util.StringMap) map[string]string { + result := map[string]string{} + for key, value := range entries { + if strings.HasPrefix(key, Prefix) { + result[RemovePrefixFromTag(key)] = value + } + } + return result +} + +func storeTagMembersInXdas(id string, members <-chan string, savedMembers chan<- string, wg *sync.WaitGroup, tagValue string) { defer wg.Done() xdasMembers := proto.XdasHashes{ - Fields: map[string]string{id: ""}, + Fields: map[string]string{id: tagValue}, } successCount := 0 From 437a92e7f2848ff53c0781e4cd3ad96d8ddfd235 Mon Sep 17 00:00:00 2001 From: Maksym Dolina Date: Fri, 15 May 2026 15:43:41 +0100 Subject: [PATCH 2/4] Refactor tag value handling in API: consolidate value retrieval logic and remove deprecated route --- taggingapi/router.go | 1 - taggingapi/tag/tag_handler_test.go | 21 +++++++++++++++++++++ taggingapi/tag/tag_member_handler.go | 17 ++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/taggingapi/router.go b/taggingapi/router.go index 4aa33fa..605e013 100644 --- a/taggingapi/router.go +++ b/taggingapi/router.go @@ -28,7 +28,6 @@ func routeTaggingServiceApis(r *mux.Router, s *xhttp.WebconfigServer) { taggingPath.HandleFunc("", tag.GetAllTagsHandler).Methods("GET").Name("Get-all-tags") taggingPath.HandleFunc("/{tag}", tag.GetTagByIdHandler).Methods("GET").Name("Get-tag-by-id") taggingPath.HandleFunc("/{tag}/members", tag.AddMembersToTagHandler).Methods("PUT").Name("Add-members-to-tag") - taggingPath.HandleFunc("/{tag}/values/{value}/members", tag.AddMembersToTagHandler).Methods("PUT").Name("Add-members-to-tag-with-value") taggingPath.HandleFunc("/{tag}", tag.DeleteTagHandler).Methods("DELETE").Name("Delete-tag-v2") taggingPath.HandleFunc("/{tag}/members", tag.RemoveMembersFromTagHandler).Methods("DELETE").Name("Remove-members-from-tag") taggingPath.HandleFunc("/{tag}/members/{member}", tag.RemoveMemberFromTagHandler).Methods("DELETE").Name("Remove-member-from-tag") diff --git a/taggingapi/tag/tag_handler_test.go b/taggingapi/tag/tag_handler_test.go index 5da295a..e0cf877 100644 --- a/taggingapi/tag/tag_handler_test.go +++ b/taggingapi/tag/tag_handler_test.go @@ -115,6 +115,27 @@ func TestAddMembersToTagHandler_ExceedsBatchSize(t *testing.T) { assert.Contains(t, recorder.Body.String(), "exceeds maximum") } +func TestGetTagValueFromRequest_QueryParameterWins(t *testing.T) { + req := httptest.NewRequest("PUT", "/tags/test-tag/members?value=business", nil) + req = mux.SetURLVars(req, map[string]string{common.TagValue: "path-value"}) + + assert.Equal(t, "business", getTagValueFromRequest(req)) +} + +func TestGetTagValueFromRequest_PathVariableFallback(t *testing.T) { + req := httptest.NewRequest("PUT", "/tags/test-tag/values/business/members", nil) + req = mux.SetURLVars(req, map[string]string{common.TagValue: "business"}) + + assert.Equal(t, "business", getTagValueFromRequest(req)) +} + +func TestGetTagValueFromRequest_DefaultsToEmpty(t *testing.T) { + req := httptest.NewRequest("PUT", "/tags/test-tag/members", nil) + req = mux.SetURLVars(req, map[string]string{common.Tag: "test-tag"}) + + assert.Equal(t, "", getTagValueFromRequest(req)) +} + func TestRemoveMembersFromTagHandler_MissingTag(t *testing.T) { setupTestEnvironment() req := httptest.NewRequest("DELETE", "/tags//members", nil) diff --git a/taggingapi/tag/tag_member_handler.go b/taggingapi/tag/tag_member_handler.go index 853ee86..d373b89 100644 --- a/taggingapi/tag/tag_member_handler.go +++ b/taggingapi/tag/tag_member_handler.go @@ -111,7 +111,7 @@ func AddMembersToTagHandler(w http.ResponseWriter, r *http.Request) { return } - tagValue := mux.Vars(r)[common.TagValue] // "" when called via /{tag}/members (old route) + tagValue := getTagValueFromRequest(r) xw, ok := w.(*xwhttp.XResponseWriter) if !ok { @@ -155,6 +155,21 @@ func AddMembersToTagHandler(w http.ResponseWriter, r *http.Request) { xhttp.WriteXconfResponse(w, http.StatusAccepted, respBytes) } +func getTagValueFromRequest(r *http.Request) string { + if values, found := r.URL.Query()[common.TagValue]; found { + if len(values) > 0 { + return values[0] + } + return "" + } + + if value, found := mux.Vars(r)[common.TagValue]; found { + return value + } + + return "" +} + // RemoveMembersFromTagHandler - Updated with bucketed implementation func RemoveMembersFromTagHandler(w http.ResponseWriter, r *http.Request) { id, found := mux.Vars(r)[common.Tag] From 9550ae5ea881c612988480e8baff72add27b3b44 Mon Sep 17 00:00:00 2001 From: Maksym Dolina Date: Fri, 15 May 2026 16:45:10 +0100 Subject: [PATCH 3/4] Refactor tag value retrieval logic: simplify request handling and improve test clarity --- taggingapi/tag/tag_handler_test.go | 10 ++++------ taggingapi/tag/tag_member_handler.go | 5 ----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/taggingapi/tag/tag_handler_test.go b/taggingapi/tag/tag_handler_test.go index e0cf877..9ad8e47 100644 --- a/taggingapi/tag/tag_handler_test.go +++ b/taggingapi/tag/tag_handler_test.go @@ -115,18 +115,16 @@ func TestAddMembersToTagHandler_ExceedsBatchSize(t *testing.T) { assert.Contains(t, recorder.Body.String(), "exceeds maximum") } -func TestGetTagValueFromRequest_QueryParameterWins(t *testing.T) { +func TestGetTagValueFromRequest_QueryParameterPresent(t *testing.T) { req := httptest.NewRequest("PUT", "/tags/test-tag/members?value=business", nil) - req = mux.SetURLVars(req, map[string]string{common.TagValue: "path-value"}) assert.Equal(t, "business", getTagValueFromRequest(req)) } -func TestGetTagValueFromRequest_PathVariableFallback(t *testing.T) { - req := httptest.NewRequest("PUT", "/tags/test-tag/values/business/members", nil) - req = mux.SetURLVars(req, map[string]string{common.TagValue: "business"}) +func TestGetTagValueFromRequest_EmptyQueryParameter(t *testing.T) { + req := httptest.NewRequest("PUT", "/tags/test-tag/members?value=", nil) - assert.Equal(t, "business", getTagValueFromRequest(req)) + assert.Equal(t, "", getTagValueFromRequest(req)) } func TestGetTagValueFromRequest_DefaultsToEmpty(t *testing.T) { diff --git a/taggingapi/tag/tag_member_handler.go b/taggingapi/tag/tag_member_handler.go index d373b89..366ec02 100644 --- a/taggingapi/tag/tag_member_handler.go +++ b/taggingapi/tag/tag_member_handler.go @@ -160,11 +160,6 @@ func getTagValueFromRequest(r *http.Request) string { if len(values) > 0 { return values[0] } - return "" - } - - if value, found := mux.Vars(r)[common.TagValue]; found { - return value } return "" From 8f000a096290a3973921f74422c0fc526633a12e Mon Sep 17 00:00:00 2001 From: Maksym Dolina Date: Fri, 15 May 2026 18:37:15 +0100 Subject: [PATCH 4/4] Change log level for tag member retrieval: switch from Info to Debug for improved logging granularity --- taggingapi/tag/tag_member_service.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/taggingapi/tag/tag_member_service.go b/taggingapi/tag/tag_member_service.go index 169a0a5..d2c3160 100644 --- a/taggingapi/tag/tag_member_service.go +++ b/taggingapi/tag/tag_member_service.go @@ -240,7 +240,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb limit = DefaultPageSizeV2 } - log.Infof("Getting paginated members for tag %s, limit %d, cursor %s", tagId, limit, cursor) + log.Debugf("Getting paginated members for tag %s, limit %d, cursor %s", tagId, limit, cursor) populatedBuckets, err := getPopulatedBuckets(tagId) if err != nil { @@ -251,7 +251,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb return nil, xwcommon.NewRemoteErrorAS(http.StatusNotFound, fmt.Sprintf(NotFoundErrorMsg, tagId)) } - log.Infof("Found %d populated buckets for tag %s", len(populatedBuckets), tagId) + log.Debugf("Found %d populated buckets for tag %s", len(populatedBuckets), tagId) state := parseBucketedCursor(cursor) var allMembers []string @@ -297,7 +297,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb if len(result.members) > needed { allMembers = append(allMembers, result.members[:needed]...) nextCursor := generateBucketedCursor(currentBucketId, result.members[needed-1], len(allMembers)) - log.Infof("Returning %d members for tag %s with more data in bucket %d", + log.Debugf("Returning %d members for tag %s with more data in bucket %d", len(allMembers), tagId, currentBucketId) return &PaginatedMembersResponse{ Data: allMembers, @@ -322,7 +322,7 @@ func GetMembersPaginated(tagId string, limit int, cursor string) (*PaginatedMemb nextCursor = generateBucketedCursor(nextBucketId, "", 0) } - log.Infof("Returning %d members for tag %s, hasMore: %v", len(allMembers), tagId, hasMore) + log.Debugf("Returning %d members for tag %s, hasMore: %v", len(allMembers), tagId, hasMore) return &PaginatedMembersResponse{ Data: allMembers, NextCursor: nextCursor,