Skip to content
Snippets Groups Projects
Select Git revision
  • 3d1d1cdd1f665f32fbcb14503c3b45bda725237e
  • main default protected
  • v1.298.0
  • v1.297.0
  • v1.296.0
  • v1.295.0
  • v1.294.0
  • v1.293.0
  • v1.292.0
  • v1.291.0
  • v1.290.0
  • v1.289.0
  • v1.288.0
  • v1.287.0
  • v1.286.0
  • v1.285.0
  • v1.284.0
  • v1.283.0
  • v1.282.0
  • v1.281.0
  • v1.280.0
  • v1.279.0
22 results

audit.go

Blame
  • audit.go 10.33 KiB
    package audit
    
    import (
    	"encoding/json"
    	"gitlab.com/uafrica/go-utils/errors"
    	"reflect"
    	"regexp"
    	"strconv"
    	"strings"
    
    	"github.com/r3labs/diff/v2"
    	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection"
    	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
    )
    
    type FieldChange struct {
    	From interface{} `json:"change_from"`
    	To   interface{} `json:"change_to"`
    }
    
    func VerifyAuditEvents(original interface{}, new interface{}) error {
    	if original != nil {
    		structValue := reflect.ValueOf(original)
    		if structValue.Kind() != reflect.Struct {
    			return errors.New("original object is not of type struct")
    		}
    	}
    
    	if new != nil {
    		structValue = reflect.ValueOf(new)
    		if structValue.Kind() != reflect.Struct {
    			return errors.New("new object is not of type struct")
    		}
    	}
    
    	return nil
    }
    
    func GetChanges(original interface{}, new interface{}) (map[string]interface{}, error) {
    	changes := map[string]interface{}{}
    	changelog, err := diff.Diff(original, new)
    	if err != nil {
    		return changes, err
    	}
    
    	for _, change := range changelog {
    
    		if len(change.Path) == 1 {
    			// Root object change
    			field := ToSnakeCase(change.Path[0])
    			changes[field] = FieldChange{
    				From: change.From,
    				To:   change.To,
    			}
    		} else if len(change.Path) == 2 {
    			// Child object changed
    			// ["Account", "ID"]
    			// 0 = Object
    			// 1 = field
    
    			ChildObjectChanges(changes, change.Path[0], change.Path[1], change.From, change.To)
    
    		} else if len(change.Path) >= 3 {
    			// Array of objects
    			// ["Parcel", "0", "ActualWeight"]
    			// 0 = Object
    			// 1 = Index of object
    			// 2 = field
    
    			objectKey := ToSnakeCase(change.Path[0])
    			indexString := change.Path[1]
    
    			if !string_utils.IsNumericString(indexString) {
    				// Not an array, but a deeper nested object
    				ChildObjectChanges(changes, change.Path[len(change.Path)-2], change.Path[len(change.Path)-1], change.From, change.To)
    				continue
    			}
    
    			index, _ := string_utils.StringToInt64(indexString)
    			field := ToSnakeCase(change.Path[2])
    
    			arrayObject, present := changes[objectKey]
    			if present {
    				if arrayOfObjects, ok := arrayObject.([]map[string]interface{}); ok {
    					arrayIndex := ArrayIndexForObjectIndex(arrayOfObjects, index)
    					if arrayIndex != -1 {
    						// Add field to existing object in array
    						object := arrayOfObjects[arrayIndex]
    						object[field] = FieldChange{
    							From: change.From,
    							To:   change.To,
    						}
    					} else {
    						// new object, append to existing array
    						fieldChange := map[string]interface{}{
    							"index": index,
    							field: FieldChange{
    								From: change.From,
    								To:   change.To,
    							},
    						}
    						changes[objectKey] = append(arrayOfObjects, fieldChange)
    					}
    
    				}
    			} else {
    				// Create array of objects
    				fieldChange := map[string]interface{}{
    					"index": index,
    					field: FieldChange{
    						From: change.From,
    						To:   change.To,
    					},
    				}
    				changes[objectKey] = []map[string]interface{}{
    					fieldChange,
    				}
    			}
    		}
    	}
    
    	return changes, nil
    }
    func ChildObjectChanges(changes map[string]interface{}, objectPath string, fieldPath string, changeFrom interface{}, changeTo interface{}) {
    
    	objectKey := ToSnakeCase(objectPath)
    	field := ToSnakeCase(fieldPath)
    
    	existingObject, present := changes[objectKey]
    	if present {
    		if object, ok := existingObject.(map[string]interface{}); ok {
    			object[field] = FieldChange{
    				From: changeFrom,
    				To:   changeTo,
    			}
    			changes[objectKey] = object
    		}
    	} else {
    		fieldChange := map[string]interface{}{
    			field: FieldChange{
    				From: changeFrom,
    				To:   changeTo,
    			},
    		}
    		changes[objectKey] = fieldChange
    	}
    }
    
    // ArrayIndexForObjectIndex gets the index of arrayOfObjects where the object's index field is equal to objectIndex.
    func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectIndex int64) int {
    	for arrayIndex, object := range arrayOfObjects {
    		index, present := object["index"]
    		if present {
    			if index == objectIndex {
    				return arrayIndex
    			}
    		}
    	}
    
    	return -1
    }
    
    // GetAllChanges Returns the diff, structured in json, recursively
    // Be warned, here be dragons. Debug this first to understand how it works
    func GetAllChanges(original interface{}, new interface{}) (map[string]interface{}, error) {
    	changes := map[string]interface{}{}
    	changelog, err := diff.Diff(original, new)
    	if err != nil {
    		return changes, err
    	}
    
    	changesJson := "{"
    	subArrays := map[string]string{}
    	for _, change := range changelog {
    		var value string   // Keep track of the core value
    		var key string     // Keep track of the key/s for this value
    		var closing string // Keep track of the correct number of closing characters
    
    		for pathIndex := len(change.Path) - 1; pathIndex >= 0; pathIndex-- {
    			// If this is the first value, it's the "deepest" value of this change set, otherwise leave this var alone
    			// If this is set at the wrong point, other behaviour changes
    			if pathIndex+1 == len(change.Path) {
    				changedField := FieldChange{
    					From: change.From,
    					To:   change.To,
    				}
    				request, err := json.Marshal(changedField)
    				if err != nil {
    					return nil, err
    				}
    				value = string(request)
    				value = value[1 : len(value)-1]
    			}
    
    			// If this "key" is integer-like, we handle it a little differently to ensure related data ends up
    			// together and formatted as you would expect JSON arrays to look
    			if _, convErr := strconv.Atoi(change.Path[pathIndex]); convErr == nil {
    				positionOfSubArray := ""
    				for sub := 0; sub <= pathIndex; sub++ {
    					positionOfSubArray = positionOfSubArray + change.Path[sub]
    				}
    
    				// Add it to the placeholder data map used later on
    				subArrays[positionOfSubArray] = subArrays[positionOfSubArray] + key + value + closing + ","
    
    				// Don't insert the same placeholder into the json multiple times
    				if !strings.Contains(changesJson, positionOfSubArray) {
    					value = positionOfSubArray
    				} else {
    					// Make sure this value doesn't end up populated explicitly
    					value = ""
    				}
    			} else {
    				// Safety net so we don't compare to a non existant value
    				if pathIndex < len(change.Path)-1 {
    					// If the value this one "contains" has a integer-like key, this is probably an array
    					if _, err = strconv.Atoi(change.Path[pathIndex+1]); err == nil {
    						key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": ["
    						closing = "]"
    					} else {
    						key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": {" + key
    						closing = closing + "}"
    					}
    				} else {
    					key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": {" + key
    					closing = closing + "}"
    				}
    			}
    		}
    
    		// Don't insert empty values (happens when multiple values within an array are set, see "placeholder" behavior)
    		if value != "" {
    			// Duplicate key prevention
    			keySplit := strings.Split(key, ": ")
    			lastOccurrence := 0
    			lastMatched := ""
    			levelsDeep := 0
    			for sIndex, split := range keySplit {
    				// Trim leading character off split
    				if split[0:1] == "{" || split[0:1] == "[" {
    					split = split[1:]
    				}
    
    				// The final value of keySplit might be empty ("")
    				if split != "" {
    					if index := strings.Index(changesJson, split); index > -1 && changesJson != "{" {
    						// Prevent reverse traversal
    						if index > lastOccurrence {
    							// Prevent finding nested keys as opposed to our "change" keys
    							opener := changesJson[index+len(split)+2 : index+len(split)+3]
    							// Prevent a "base level" key from matching a nested key
    							baseLevel := false
    							if index > 1 {
    								containedBy := changesJson[index-1 : index]
    								if levelsDeep == 0 && (containedBy == "{" || containedBy == "[") {
    									baseLevel = true
    								}
    							}
    							if (opener == "{" || opener == "[") && !baseLevel {
    								lastOccurrence = index
    								lastMatched = split
    								levelsDeep = sIndex
    							}
    						}
    					}
    				}
    			}
    
    			// If the "key" is already present, handle it differently
    			if lastOccurrence > 0 {
    				// Strip parent keys that are already present
    				key = key[strings.Index(key, lastMatched)+len(lastMatched):]
    				if key[0:2] == ": " {
    					key = key[2:]
    				}
    				// Strip the correct amount of closing tags
    				closing = closing[:len(closing)-levelsDeep]
    
    				// Find the position of this key in the master string
    				position := strings.Index(changesJson, lastMatched) + len(lastMatched) + 3
    
    				// We're appending to an existing object, so strip the outermost wrapping layer
    				key = key[1:]
    				closing = closing[:len(closing)-1]
    
    				// Place the value within the existing key
    				changesJson = changesJson[:position] + key + value + closing + ", " + changesJson[position:]
    			} else {
    				// No value found, append it to the end
    				changesJson = changesJson + key + value + closing + ", "
    			}
    		}
    	}
    	// Trim whitespace and strip trailing comma since we are done inserting at the back
    	changesJson = strings.TrimSpace(changesJson)
    	if changesJson[len(changesJson)-1:] == "," {
    		changesJson = changesJson[:len(changesJson)-1]
    	}
    	changesJson = changesJson + "}"
    
    	// Now we can go make sure the placeholders are populated with the data in
    	for placeholderKey, placeholderValue := range subArrays {
    		// Trim the trailing comma since we won't be adding any more stuff
    		placeholderValue = strings.TrimSpace(placeholderValue)
    		if placeholderValue[len(placeholderValue)-1:] == "," {
    			placeholderValue = placeholderValue[:len(placeholderValue)-1]
    		}
    		changesJson = strings.ReplaceAll(changesJson, placeholderKey, "{"+placeholderValue+"}")
    	}
    
    	// Now, hopefully, the json parsing will pass and we have a nicely formatted dataset
    	err = json.Unmarshal([]byte(changesJson), &changes)
    	if err != nil {
    		return nil, err
    	}
    
    	return changes, nil
    }
    
    var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
    var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
    
    func ToSnakeCase(str string) string {
    	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
    	snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
    	return strings.ToLower(snake)
    }
    
    func GetIntValue(object interface{}, key string) int64 {
    	structValue := reflect.ValueOf(object)
    	if structValue.Kind() == reflect.Struct {
    		field := structValue.FieldByName(key)
    		id := reflection.GetInt64Value(field)
    		return id
    	}
    	return 0
    }
    
    func GetStringValue(object interface{}, key string) string {
    	structValue := reflect.ValueOf(object)
    	if structValue.Kind() == reflect.Struct {
    		field := structValue.FieldByName(key)
    		id := reflection.GetStringValue(field)
    		return id
    	}
    	return ""
    }