Skip to content

proper patch support for user attribute patch api with nested structure #6689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions api/restHandler/UserAttributesRestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package restHandler
import (
"encoding/json"
"errors"
"github.com/devtron-labs/devtron/pkg/attributes/bean"
"net/http"

"github.com/devtron-labs/devtron/api/restHandler/common"
Expand Down Expand Up @@ -108,15 +109,15 @@ func (handler *UserAttributesRestHandlerImpl) PatchUserAttributes(w http.Respons
common.WriteJsonResp(w, nil, resp, http.StatusOK)
}

func (handler *UserAttributesRestHandlerImpl) validateUserAttributesRequest(w http.ResponseWriter, r *http.Request, operation string) (*attributes.UserAttributesDto, bool) {
func (handler *UserAttributesRestHandlerImpl) validateUserAttributesRequest(w http.ResponseWriter, r *http.Request, operation string) (*bean.UserAttributesDto, bool) {
userId, err := handler.userService.GetLoggedInUser(r)
if userId == 0 || err != nil {
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
return nil, false
}

decoder := json.NewDecoder(r.Body)
var dto attributes.UserAttributesDto
var dto bean.UserAttributesDto
err = decoder.Decode(&dto)
if err != nil {
handler.logger.Errorw("request err, "+operation, "err", err, "payload", dto)
Expand Down Expand Up @@ -158,7 +159,7 @@ func (handler *UserAttributesRestHandlerImpl) GetUserAttribute(w http.ResponseWr
return
}

dto := attributes.UserAttributesDto{}
dto := bean.UserAttributesDto{}

emailId, err := handler.userService.GetActiveEmailById(userId)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion env_gen.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions env_gen.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,10 @@
| DASHBOARD_PORT | string |3000 | Port for dashboard micro-service | | false |
| DEX_HOST | string |http://localhost | | | false |
| DEX_PORT | string |5556 | | | false |
| GIT_SENSOR_PROTOCOL | string |GRPC | Protocol to connect with git-sensor micro-service | | false |
| GIT_SENSOR_PROTOCOL | string |REST | Protocol to connect with git-sensor micro-service | | false |
| GIT_SENSOR_SERVICE_CONFIG | string |{"loadBalancingPolicy":"pick_first"} | git-sensor grpc service config | | false |
| GIT_SENSOR_TIMEOUT | int |0 | Timeout for getting response from the git-sensor | | false |
| GIT_SENSOR_URL | string |127.0.0.1:7071 | git-sensor micro-service url | | false |
| GIT_SENSOR_URL | string |127.0.0.1:7070 | git-sensor micro-service url | | false |
| HELM_CLIENT_URL | string |127.0.0.1:50051 | Kubelink micro-service url | | false |
| KUBELINK_GRPC_MAX_RECEIVE_MSG_SIZE | int |20 | | | false |
| KUBELINK_GRPC_MAX_SEND_MSG_SIZE | int |4 | | | false |
Expand Down
195 changes: 127 additions & 68 deletions pkg/attributes/UserAttributesService.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,25 @@ import (
"encoding/json"
"errors"
"github.com/devtron-labs/devtron/internal/sql/repository"
"github.com/devtron-labs/devtron/pkg/attributes/adapter"
"github.com/devtron-labs/devtron/pkg/attributes/bean"
"github.com/go-pg/pg"
"go.uber.org/zap"
"reflect"
)

type UserAttributesService interface {
AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
PatchUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error)
AddUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
UpdateUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
PatchUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
GetUserAttribute(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
}

type UserAttributesServiceImpl struct {
logger *zap.SugaredLogger
attributesRepository repository.UserAttributesRepository
}

type UserAttributesDto struct {
EmailId string `json:"emailId"`
Key string `json:"key"`
Value string `json:"value"`
UserId int32 `json:"-"`
}

func NewUserAttributesServiceImpl(logger *zap.SugaredLogger,
attributesRepository repository.UserAttributesRepository) *UserAttributesServiceImpl {
serviceImpl := &UserAttributesServiceImpl{
Expand All @@ -53,7 +48,7 @@ func NewUserAttributesServiceImpl(logger *zap.SugaredLogger,
return serviceImpl
}

func (impl UserAttributesServiceImpl) AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
func (impl UserAttributesServiceImpl) AddUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
dao := &repository.UserAttributesDao{
EmailId: request.EmailId,
Key: request.Key,
Expand All @@ -68,7 +63,7 @@ func (impl UserAttributesServiceImpl) AddUserAttributes(request *UserAttributesD
return request, nil
}

func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {

userAttribute, err := impl.GetUserAttribute(request)
if err != nil {
Expand Down Expand Up @@ -98,96 +93,160 @@ func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttribut
return request, nil
}

func (impl UserAttributesServiceImpl) PatchUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
userAttribute, err := impl.GetUserAttribute(request)
func (impl UserAttributesServiceImpl) PatchUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
existingAttribute, err := impl.GetUserAttribute(request)
if err != nil {
impl.logger.Errorw("error while getting user attributes during patch request", "req", request, "error", err)
return nil, errors.New("error occurred while updating user attributes")
return nil, errors.New("error occurred while getting user attributes")
}
if userAttribute == nil {

if existingAttribute == nil {
impl.logger.Info("no data found for request, so going to add instead of update", "req", request)
attributes, err := impl.AddUserAttributes(request)
newAttribute, err := impl.AddUserAttributes(request)
if err != nil {
impl.logger.Errorw("error in adding new user attributes", "req", request, "error", err)
return nil, errors.New("error occurred while updating user attributes")
return nil, errors.New("error occurred while adding user attributes")
}
return attributes, nil
return newAttribute, nil
}

// Parse existing JSON
var existingData map[string]interface{}
if userAttribute.Value != "" {
err = json.Unmarshal([]byte(userAttribute.Value), &existingData)
if err != nil {
impl.logger.Errorw("error parsing existing json value", "value", userAttribute.Value, "error", err)
return nil, errors.New("error occurred while updating user attributes")
}
} else {
existingData = make(map[string]interface{})
existingData, err := impl.parseJSONValue(existingAttribute.Value, "existing")
if err != nil {
impl.logger.Errorw("error in parsing json", "existingAttribute.Value", existingAttribute.Value, "error", err)
return nil, err
}

// Parse new JSON
var newData map[string]interface{}
if request.Value != "" {
err = json.Unmarshal([]byte(request.Value), &newData)
if err != nil {
impl.logger.Errorw("error parsing request json value", "value", request.Value, "error", err)
return nil, errors.New("error occurred while updating user attributes")
}
} else {
newData = make(map[string]interface{})
newData, err := impl.parseJSONValue(request.Value, "request")
if err != nil {
impl.logger.Errorw("error in parsing request json", "request.Value", request.Value, "error", err)
return nil, err
}

// Check if there are any changes
anyChanges := false
hasChanges := impl.mergeUserAttributesData(existingData, newData)
if !hasChanges {
impl.logger.Infow("no changes detected, skipping update", "key", request.Key)
return existingAttribute, nil
}

// Merge the objects (patch style)
for key, newValue := range newData {
existingValue, exists := existingData[key]
if !exists || !reflect.DeepEqual(existingValue, newValue) {
existingData[key] = newValue
anyChanges = true
}
return impl.updateAttributeInDatabase(request, existingData)
}

// parseJSONValue parses a JSON string into a map, with proper error handling
func (impl UserAttributesServiceImpl) parseJSONValue(jsonValue, context string) (map[string]interface{}, error) {
var data map[string]interface{}

if jsonValue == "" {
return make(map[string]interface{}), nil
}

// If no changes, return the existing data
if !anyChanges {
impl.logger.Infow("no change detected, skipping update", "key", request.Key)
return userAttribute, nil
err := json.Unmarshal([]byte(jsonValue), &data)
if err != nil {
impl.logger.Errorw("error parsing JSON value", "context", context, "value", jsonValue, "error", err)
return nil, errors.New("error occurred while parsing user attributes data")
}

// Convert back to JSON string
mergedJson, err := json.Marshal(existingData)
return data, nil
}

// updateAttributeInDatabase updates the merged data in the database
func (impl UserAttributesServiceImpl) updateAttributeInDatabase(request *bean.UserAttributesDto, mergedData map[string]interface{}) (*bean.UserAttributesDto, error) {
mergedJSON, err := json.Marshal(mergedData)
if err != nil {
impl.logger.Errorw("error converting merged data to json", "data", existingData, "error", err)
return nil, errors.New("error occurred while updating user attributes")
impl.logger.Errorw("error converting merged data to JSON", "data", mergedData, "error", err)
return nil, errors.New("error occurred while processing user attributes")
}

dao := &repository.UserAttributesDao{
EmailId: request.EmailId,
Key: request.Key,
Value: string(mergedJson),
Value: string(mergedJSON),
UserId: request.UserId,
}

// Update in database
err = impl.attributesRepository.UpdateDataValByKey(dao)
if err != nil {
impl.logger.Errorw("error in update attributes", "req", dao, "error", err)
impl.logger.Errorw("error updating user attributes in database", "dao", dao, "error", err)
return nil, errors.New("error occurred while updating user attributes")
}

// Return the updated data
result := &UserAttributesDto{
EmailId: request.EmailId,
Key: request.Key,
Value: string(mergedJson),
UserId: request.UserId,
// Build and return response
return adapter.BuildResponseDTO(request, string(mergedJSON)), nil
}

// mergeUserAttributesData merges newData into existingData with special handling for resources
func (impl UserAttributesServiceImpl) mergeUserAttributesData(existingData, newData map[string]interface{}) bool {
hasChanges := false

for key, newValue := range newData {
if key == bean.UserPreferencesResourcesKey {
// Special handling for resources - merge nested structure
if impl.mergeResourcesData(existingData, newValue) {
hasChanges = true
}
} else {
if impl.mergeStandardAttribute(existingData, key, newValue) {
hasChanges = true
}
}
}

return hasChanges
}

// mergeStandardAttribute merges a standard (non-resource) attribute
func (impl UserAttributesServiceImpl) mergeStandardAttribute(existingData map[string]interface{}, key string, newValue interface{}) bool {
existingValue, exists := existingData[key]
if !exists || !reflect.DeepEqual(existingValue, newValue) {
existingData[key] = newValue
return true
}
return false
}

// mergeResourcesData handles the special merging logic for the resources object
func (impl UserAttributesServiceImpl) mergeResourcesData(existingData map[string]interface{}, newResourcesValue interface{}) bool {
impl.ensureResourcesStructureExists(existingData)

existingResources, ok := existingData[bean.UserPreferencesResourcesKey].(map[string]interface{})
if !ok {
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
return true
}

newResources, ok := newResourcesValue.(map[string]interface{})
if !ok {
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
return true
}

return impl.mergeResourceTypes(existingResources, newResources)
}

// ensureResourcesStructureExists initializes the resources structure if it doesn't exist
func (impl UserAttributesServiceImpl) ensureResourcesStructureExists(existingData map[string]interface{}) {
if existingData[bean.UserPreferencesResourcesKey] == nil {
existingData[bean.UserPreferencesResourcesKey] = make(map[string]interface{})
}
}

// mergeResourceTypes merges individual resource types from new resources into existing resources
func (impl UserAttributesServiceImpl) mergeResourceTypes(existingResources, newResources map[string]interface{}) bool {
hasChanges := false

// Merge each resource type from newResources
for resourceType, newResourceData := range newResources {
existingResourceData, exists := existingResources[resourceType]
if !exists || !reflect.DeepEqual(existingResourceData, newResourceData) {
existingResources[resourceType] = newResourceData
hasChanges = true
}
}

return result, nil
return hasChanges
}

func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error) {
func (impl UserAttributesServiceImpl) GetUserAttribute(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {

dao := &repository.UserAttributesDao{
EmailId: request.EmailId,
Expand All @@ -203,7 +262,7 @@ func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDt
impl.logger.Errorw("error in fetching user attributes", "req", request, "error", err)
return nil, errors.New("error occurred while getting user attributes")
}
resAttrDto := &UserAttributesDto{
resAttrDto := &bean.UserAttributesDto{
EmailId: request.EmailId,
Key: request.Key,
Value: modelValue,
Expand Down
Loading
Loading