Skip to content

Commit 7b8bb67

Browse files
Merge pull request #6689 from devtron-labs/user-attribute-patch-fix
feat: enhance patch support for user attribute patch api within nested structure
2 parents d2b95d9 + d0e7d20 commit 7b8bb67

File tree

5 files changed

+358
-71
lines changed

5 files changed

+358
-71
lines changed

api/restHandler/UserAttributesRestHandler.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package restHandler
1919
import (
2020
"encoding/json"
2121
"errors"
22+
"github.com/devtron-labs/devtron/pkg/attributes/bean"
2223
"net/http"
2324

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

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

118119
decoder := json.NewDecoder(r.Body)
119-
var dto attributes.UserAttributesDto
120+
var dto bean.UserAttributesDto
120121
err = decoder.Decode(&dto)
121122
if err != nil {
122123
handler.logger.Errorw("request err, "+operation, "err", err, "payload", dto)
@@ -158,7 +159,7 @@ func (handler *UserAttributesRestHandlerImpl) GetUserAttribute(w http.ResponseWr
158159
return
159160
}
160161

161-
dto := attributes.UserAttributesDto{}
162+
dto := bean.UserAttributesDto{}
162163

163164
emailId, err := handler.userService.GetActiveEmailById(userId)
164165
if err != nil {

pkg/attributes/UserAttributesService.go

Lines changed: 127 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,25 @@ import (
2020
"encoding/json"
2121
"errors"
2222
"github.com/devtron-labs/devtron/internal/sql/repository"
23+
"github.com/devtron-labs/devtron/pkg/attributes/adapter"
24+
"github.com/devtron-labs/devtron/pkg/attributes/bean"
2325
"github.com/go-pg/pg"
2426
"go.uber.org/zap"
2527
"reflect"
2628
)
2729

2830
type UserAttributesService interface {
29-
AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
30-
UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
31-
PatchUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error)
32-
GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error)
31+
AddUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
32+
UpdateUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
33+
PatchUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
34+
GetUserAttribute(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error)
3335
}
3436

3537
type UserAttributesServiceImpl struct {
3638
logger *zap.SugaredLogger
3739
attributesRepository repository.UserAttributesRepository
3840
}
3941

40-
type UserAttributesDto struct {
41-
EmailId string `json:"emailId"`
42-
Key string `json:"key"`
43-
Value string `json:"value"`
44-
UserId int32 `json:"-"`
45-
}
46-
4742
func NewUserAttributesServiceImpl(logger *zap.SugaredLogger,
4843
attributesRepository repository.UserAttributesRepository) *UserAttributesServiceImpl {
4944
serviceImpl := &UserAttributesServiceImpl{
@@ -53,7 +48,7 @@ func NewUserAttributesServiceImpl(logger *zap.SugaredLogger,
5348
return serviceImpl
5449
}
5550

56-
func (impl UserAttributesServiceImpl) AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
51+
func (impl UserAttributesServiceImpl) AddUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
5752
dao := &repository.UserAttributesDao{
5853
EmailId: request.EmailId,
5954
Key: request.Key,
@@ -68,7 +63,7 @@ func (impl UserAttributesServiceImpl) AddUserAttributes(request *UserAttributesD
6863
return request, nil
6964
}
7065

71-
func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
66+
func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
7267

7368
userAttribute, err := impl.GetUserAttribute(request)
7469
if err != nil {
@@ -98,96 +93,160 @@ func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttribut
9893
return request, nil
9994
}
10095

101-
func (impl UserAttributesServiceImpl) PatchUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
102-
userAttribute, err := impl.GetUserAttribute(request)
96+
func (impl UserAttributesServiceImpl) PatchUserAttributes(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
97+
existingAttribute, err := impl.GetUserAttribute(request)
10398
if err != nil {
10499
impl.logger.Errorw("error while getting user attributes during patch request", "req", request, "error", err)
105-
return nil, errors.New("error occurred while updating user attributes")
100+
return nil, errors.New("error occurred while getting user attributes")
106101
}
107-
if userAttribute == nil {
102+
103+
if existingAttribute == nil {
108104
impl.logger.Info("no data found for request, so going to add instead of update", "req", request)
109-
attributes, err := impl.AddUserAttributes(request)
105+
newAttribute, err := impl.AddUserAttributes(request)
110106
if err != nil {
111107
impl.logger.Errorw("error in adding new user attributes", "req", request, "error", err)
112-
return nil, errors.New("error occurred while updating user attributes")
108+
return nil, errors.New("error occurred while adding user attributes")
113109
}
114-
return attributes, nil
110+
return newAttribute, nil
115111
}
116112

117-
// Parse existing JSON
118-
var existingData map[string]interface{}
119-
if userAttribute.Value != "" {
120-
err = json.Unmarshal([]byte(userAttribute.Value), &existingData)
121-
if err != nil {
122-
impl.logger.Errorw("error parsing existing json value", "value", userAttribute.Value, "error", err)
123-
return nil, errors.New("error occurred while updating user attributes")
124-
}
125-
} else {
126-
existingData = make(map[string]interface{})
113+
existingData, err := impl.parseJSONValue(existingAttribute.Value, "existing")
114+
if err != nil {
115+
impl.logger.Errorw("error in parsing json", "existingAttribute.Value", existingAttribute.Value, "error", err)
116+
return nil, err
127117
}
128118

129-
// Parse new JSON
130-
var newData map[string]interface{}
131-
if request.Value != "" {
132-
err = json.Unmarshal([]byte(request.Value), &newData)
133-
if err != nil {
134-
impl.logger.Errorw("error parsing request json value", "value", request.Value, "error", err)
135-
return nil, errors.New("error occurred while updating user attributes")
136-
}
137-
} else {
138-
newData = make(map[string]interface{})
119+
newData, err := impl.parseJSONValue(request.Value, "request")
120+
if err != nil {
121+
impl.logger.Errorw("error in parsing request json", "request.Value", request.Value, "error", err)
122+
return nil, err
139123
}
140124

141-
// Check if there are any changes
142-
anyChanges := false
125+
hasChanges := impl.mergeUserAttributesData(existingData, newData)
126+
if !hasChanges {
127+
impl.logger.Infow("no changes detected, skipping update", "key", request.Key)
128+
return existingAttribute, nil
129+
}
143130

144-
// Merge the objects (patch style)
145-
for key, newValue := range newData {
146-
existingValue, exists := existingData[key]
147-
if !exists || !reflect.DeepEqual(existingValue, newValue) {
148-
existingData[key] = newValue
149-
anyChanges = true
150-
}
131+
return impl.updateAttributeInDatabase(request, existingData)
132+
}
133+
134+
// parseJSONValue parses a JSON string into a map, with proper error handling
135+
func (impl UserAttributesServiceImpl) parseJSONValue(jsonValue, context string) (map[string]interface{}, error) {
136+
var data map[string]interface{}
137+
138+
if jsonValue == "" {
139+
return make(map[string]interface{}), nil
151140
}
152141

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

159-
// Convert back to JSON string
160-
mergedJson, err := json.Marshal(existingData)
148+
return data, nil
149+
}
150+
151+
// updateAttributeInDatabase updates the merged data in the database
152+
func (impl UserAttributesServiceImpl) updateAttributeInDatabase(request *bean.UserAttributesDto, mergedData map[string]interface{}) (*bean.UserAttributesDto, error) {
153+
mergedJSON, err := json.Marshal(mergedData)
161154
if err != nil {
162-
impl.logger.Errorw("error converting merged data to json", "data", existingData, "error", err)
163-
return nil, errors.New("error occurred while updating user attributes")
155+
impl.logger.Errorw("error converting merged data to JSON", "data", mergedData, "error", err)
156+
return nil, errors.New("error occurred while processing user attributes")
164157
}
165158

166159
dao := &repository.UserAttributesDao{
167160
EmailId: request.EmailId,
168161
Key: request.Key,
169-
Value: string(mergedJson),
162+
Value: string(mergedJSON),
170163
UserId: request.UserId,
171164
}
172165

166+
// Update in database
173167
err = impl.attributesRepository.UpdateDataValByKey(dao)
174168
if err != nil {
175-
impl.logger.Errorw("error in update attributes", "req", dao, "error", err)
169+
impl.logger.Errorw("error updating user attributes in database", "dao", dao, "error", err)
176170
return nil, errors.New("error occurred while updating user attributes")
177171
}
178172

179-
// Return the updated data
180-
result := &UserAttributesDto{
181-
EmailId: request.EmailId,
182-
Key: request.Key,
183-
Value: string(mergedJson),
184-
UserId: request.UserId,
173+
// Build and return response
174+
return adapter.BuildResponseDTO(request, string(mergedJSON)), nil
175+
}
176+
177+
// mergeUserAttributesData merges newData into existingData with special handling for resources
178+
func (impl UserAttributesServiceImpl) mergeUserAttributesData(existingData, newData map[string]interface{}) bool {
179+
hasChanges := false
180+
181+
for key, newValue := range newData {
182+
if key == bean.UserPreferencesResourcesKey {
183+
// Special handling for resources - merge nested structure
184+
if impl.mergeResourcesData(existingData, newValue) {
185+
hasChanges = true
186+
}
187+
} else {
188+
if impl.mergeStandardAttribute(existingData, key, newValue) {
189+
hasChanges = true
190+
}
191+
}
192+
}
193+
194+
return hasChanges
195+
}
196+
197+
// mergeStandardAttribute merges a standard (non-resource) attribute
198+
func (impl UserAttributesServiceImpl) mergeStandardAttribute(existingData map[string]interface{}, key string, newValue interface{}) bool {
199+
existingValue, exists := existingData[key]
200+
if !exists || !reflect.DeepEqual(existingValue, newValue) {
201+
existingData[key] = newValue
202+
return true
203+
}
204+
return false
205+
}
206+
207+
// mergeResourcesData handles the special merging logic for the resources object
208+
func (impl UserAttributesServiceImpl) mergeResourcesData(existingData map[string]interface{}, newResourcesValue interface{}) bool {
209+
impl.ensureResourcesStructureExists(existingData)
210+
211+
existingResources, ok := existingData[bean.UserPreferencesResourcesKey].(map[string]interface{})
212+
if !ok {
213+
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
214+
return true
215+
}
216+
217+
newResources, ok := newResourcesValue.(map[string]interface{})
218+
if !ok {
219+
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
220+
return true
221+
}
222+
223+
return impl.mergeResourceTypes(existingResources, newResources)
224+
}
225+
226+
// ensureResourcesStructureExists initializes the resources structure if it doesn't exist
227+
func (impl UserAttributesServiceImpl) ensureResourcesStructureExists(existingData map[string]interface{}) {
228+
if existingData[bean.UserPreferencesResourcesKey] == nil {
229+
existingData[bean.UserPreferencesResourcesKey] = make(map[string]interface{})
230+
}
231+
}
232+
233+
// mergeResourceTypes merges individual resource types from new resources into existing resources
234+
func (impl UserAttributesServiceImpl) mergeResourceTypes(existingResources, newResources map[string]interface{}) bool {
235+
hasChanges := false
236+
237+
// Merge each resource type from newResources
238+
for resourceType, newResourceData := range newResources {
239+
existingResourceData, exists := existingResources[resourceType]
240+
if !exists || !reflect.DeepEqual(existingResourceData, newResourceData) {
241+
existingResources[resourceType] = newResourceData
242+
hasChanges = true
243+
}
185244
}
186245

187-
return result, nil
246+
return hasChanges
188247
}
189248

190-
func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error) {
249+
func (impl UserAttributesServiceImpl) GetUserAttribute(request *bean.UserAttributesDto) (*bean.UserAttributesDto, error) {
191250

192251
dao := &repository.UserAttributesDao{
193252
EmailId: request.EmailId,
@@ -203,7 +262,7 @@ func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDt
203262
impl.logger.Errorw("error in fetching user attributes", "req", request, "error", err)
204263
return nil, errors.New("error occurred while getting user attributes")
205264
}
206-
resAttrDto := &UserAttributesDto{
265+
resAttrDto := &bean.UserAttributesDto{
207266
EmailId: request.EmailId,
208267
Key: request.Key,
209268
Value: modelValue,

0 commit comments

Comments
 (0)