Newer
Older
package audit
import (
"github.com/r3labs/diff/v2"
"github.com/samber/lo"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
"reflect"
"regexp"
"strconv"
"strings"
From any `json:"change_from"`
To any `json:"change_to"`
type IAuditFormatter interface {
FormatForAuditEvent() string
}
func VerifyAuditEvents(original any, new any) error {
if original != nil {
structValue := reflect.ValueOf(original)
if structValue.Kind() != reflect.Struct {
return errors.Error("original object is not of type struct")
if structValue.Kind() != reflect.Struct {
return errors.Error("new object is not of type struct")
func GetChanges(original any, new any) (map[string]any, error) {
original = cleanStruct(original)
new = cleanStruct(new)
changes := map[string]any{}
}
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

Jano Hendriks
committed
var index int64 = -1
if string_utils.IsNumericString(change.Path[1]) {
index, _ = number_utils.StringToInt64(change.Path[1])
}
objectKey := ToSnakeCase(change.Path[0])

Jano Hendriks
committed
didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, int(index))
if didInsert {
continue
}
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
didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, -1)
if didInsert {
continue
}
ChildObjectChanges(changes, change.Path[len(change.Path)-3], change.Path[len(change.Path)-1], change.From, change.To)
field := ToSnakeCase(change.Path[2])
didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, int(index))
if didInsert {
continue
}
if len(change.Path) == 5 && string_utils.IsNumericString(change.Path[3]) {
// The field is actually an array of objects.
field += fmt.Sprintf("[%s] (%s)", change.Path[3], ToSnakeCase(change.Path[4]))
}
arrayObject, present := changes[objectKey]
if present {
if arrayOfObjects, ok := arrayObject.([]map[string]any); ok {

Jano Hendriks
committed
arrayIndex := ArrayIndexForObjectIndex(arrayOfObjects, index)
if arrayIndex != -1 {
// Add field to existing object in array

Jano Hendriks
committed
object := arrayOfObjects[arrayIndex]
object[field] = FieldChange{
From: change.From,
To: change.To,
}
} else {
// new object, append to existing array
fieldChange := map[string]any{

Jano Hendriks
committed
"index": index,
field: FieldChange{
From: change.From,
To: change.To,
},
}
changes[objectKey] = append(arrayOfObjects, fieldChange)
}
}
} else {
// Create array of objects
fieldChange := map[string]any{

Jano Hendriks
committed
"index": index,
field: FieldChange{
From: change.From,
To: change.To,
},
}
changes[objectKey] = []map[string]any{
fieldChange,
}
}
}
}
return changes, nil
}
func cleanStruct(object any) any {
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
defer func() {
if err := recover(); err != nil {
logs.ErrorMsg(fmt.Sprintf("audit event panic: %+v", err))
}
}()
// If the object is empty, we have nothing to do
if object == nil {
return object
}
// Convert the object to a pointer
if reflect.ValueOf(object).Kind() != reflect.Ptr {
val := reflect.ValueOf(object)
// Create a new pointer to a new value of the type of the object
ptr := reflect.New(reflect.TypeOf(object))
// Set the newly created pointer to point to the object
ptr.Elem().Set(val)
// Overwrite the original object
object = ptr.Interface()
}
// Get the value of the object
val := reflect.ValueOf(object)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// We can only clean structs
if val.Kind() != reflect.Struct {
return object
}
// Loop through the field tags to see if we should include the related object or not.
// We default to exclude, unless specified to include
for i := 0; i < val.NumField(); i++ {
fieldVal := val.Field(i)
structField := val.Type().Field(i)
// Determine whether the field should be included or excluded
auditTag := structField.Tag.Get("audit")
auditTagOptions := strings.Split(auditTag, ",")
shouldIncludeForAudit := lo.Contains(auditTagOptions, "true")
shouldExcludeForAudit := lo.Contains(auditTagOptions, "false")
// If the audit tag is present and specified to 'true', we should always include the relation
if shouldIncludeForAudit {
continue
}
// By default, all bun relations are excluded
isBunRelationField := strings.Contains(structField.Tag.Get("bun"), "rel:")
if shouldExcludeForAudit || isBunRelationField {
if fieldVal.CanSet() {
// Set the field to its zero value (nil for pointers)
fieldVal.Set(reflect.Zero(fieldVal.Type()))
}
}
}
return object
}
func CheckToFormatForAuditEvent(changes map[string]any, original any, new any, field string, objectKey string, index int) (didInsert bool) {
originalStructField, originalFieldValue, found := getFieldFromStruct(original, field)
if !found {
return false
}
_, newFieldValue, found := getFieldFromStruct(new, field)
if !found {
return false
}
defer func() {
if err := recover(); err != nil {
logs.ErrorWithFields(map[string]any{
"error": err,
"field": field,
"index": index,
"original": fmt.Sprintf("%#v", originalFieldValue),
"new": fmt.Sprintf("%#v", newFieldValue),
}, errors.Error("Failed to format for audit event"))
}
}()
doGroupSlice := doGroupSliceForAuditEvent(originalStructField)
if !doGroupSlice {
originalFormattedObject := checkToFormatForAuditEvent(originalFieldValue, index)
newFormattedObject := checkToFormatForAuditEvent(newFieldValue, index)
if originalFormattedObject != nil ||
newFormattedObject != nil {
if index > -1 {
objectKey = fmt.Sprintf("%s[%d]", field, index)
}
changes[objectKey] = FieldChange{
From: originalFormattedObject,
To: newFormattedObject,
}
return true
} else {
originalFormattedObject := checkToFormatForGroupedAuditEvent(originalFieldValue)
newFormattedObject := checkToFormatForGroupedAuditEvent(newFieldValue)
if originalFormattedObject != nil ||
newFormattedObject != nil {
if _, present := changes[objectKey]; !present {
fieldChange := FieldChange{}
if originalFormattedObject != nil {
fieldChange.From = originalFormattedObject
} else {
fieldChange.From = []*string{}
}
if newFormattedObject != nil {
fieldChange.To = newFormattedObject
} else {
fieldChange.To = []*string{}
}
changes[objectKey] = fieldChange
}
return true
}
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
}
return false
}
func appendNewChangeToExistingChanges(existingChanges any, newChange string) any {
existingChangesStrings, ok := existingChanges.([]string)
if !ok {
return existingChanges
}
if !lo.Contains(existingChangesStrings, newChange) {
existingChangesStrings = append(existingChangesStrings, newChange)
}
return existingChangesStrings
}
func getFieldFromStruct(object any, field string) (reflect.StructField, reflect.Value, bool) {
objectValue := reflect.ValueOf(object)
if objectValue.Kind() == reflect.Ptr {
objectValue = objectValue.Elem()
}
if objectValue.Kind() != reflect.Struct {
// Fields can only be retrieved from structs
return reflect.StructField{}, reflect.Value{}, false
}
objectStructField, found := objectValue.Type().FieldByName(field)
if !found {
return reflect.StructField{}, reflect.Value{}, false
}
objectFieldValue := objectValue.FieldByName(field)
return objectStructField, objectFieldValue, true
}
func doGroupSliceForAuditEvent(objectField reflect.StructField) bool {
auditTag, found := objectField.Tag.Lookup("audit")
if found {
auditTagOptions := strings.Split(auditTag, ",")
if lo.Contains(auditTagOptions, "group") {
return true
}
}
return false
}
func checkToExecuteAuditFormatter(fieldValue reflect.Value) *string {
if auditFormatterObject, ok := fieldValue.Interface().(IAuditFormatter); ok {
fieldString := auditFormatterObject.FormatForAuditEvent()
if fieldString != "" {
return &fieldString
}
}
return nil
}
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
func checkToFormatForGroupedAuditEvent(fieldValue reflect.Value) []*string {
if fieldValue.Kind() == reflect.Ptr {
if fieldValue.IsNil() {
return nil
}
fieldValue = fieldValue.Elem()
}
var formattedField []*string
if fieldValue.Kind() == reflect.Struct {
formattedField = []*string{checkToExecuteAuditFormatter(fieldValue)}
} else if fieldValue.Kind() == reflect.Slice {
for i := 0; i < fieldValue.Len(); i++ {
sliceFieldValue := fieldValue.Index(i)
if sliceFieldValue.Kind() == reflect.Ptr {
if sliceFieldValue.IsNil() {
continue
}
sliceFieldValue = sliceFieldValue.Elem()
}
if sliceFieldValue.Kind() != reflect.Struct {
// Not a slice of structs - ignore the format flag
return nil
}
formattedField = append(formattedField, checkToExecuteAuditFormatter(sliceFieldValue))
}
}
return formattedField
}
func checkToFormatForAuditEvent(fieldValue reflect.Value, index int) *string {
if fieldValue.Kind() == reflect.Ptr {

Jano Hendriks
committed
if fieldValue.IsNil() {
return nil
}
fieldValue = fieldValue.Elem()
}
var formattedField *string
if fieldValue.Kind() == reflect.Struct {
formattedField = checkToExecuteAuditFormatter(fieldValue)
} else if fieldValue.Kind() == reflect.Slice &&
fieldValue.Len() > 0 {

Jano Hendriks
committed
if index == -1 ||
index >= fieldValue.Len() {
return nil
}
sliceFieldValue := fieldValue.Index(index)
if sliceFieldValue.Kind() == reflect.Ptr {

Jano Hendriks
committed
if fieldValue.IsNil() {
return nil
}
sliceFieldValue = sliceFieldValue.Elem()
}
if sliceFieldValue.Kind() != reflect.Struct {
// Not a slice of structs - ignore the format flag
return nil
}
formattedField = checkToExecuteAuditFormatter(sliceFieldValue)
}
return formattedField
}
func ChildObjectChanges(changes map[string]any, objectPath string, fieldPath string, changeFrom any, changeTo any) {
objectKey := ToSnakeCase(objectPath)
field := ToSnakeCase(fieldPath)
existingObject, present := changes[objectKey]
if present {
if object, ok := existingObject.(map[string]any); ok {
object[field] = FieldChange{
From: changeFrom,
To: changeTo,
}
changes[objectKey] = object
}
} else {
fieldChange := map[string]any{
field: FieldChange{
From: changeFrom,
To: changeTo,
},
}
changes[objectKey] = fieldChange
}
}

Jano Hendriks
committed
// ArrayIndexForObjectIndex gets the index of arrayOfObjects where the object's index field is equal to objectIndex.
func ArrayIndexForObjectIndex(arrayOfObjects []map[string]any, objectIndex int64) int {

Jano Hendriks
committed
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 any, new any) (map[string]any, error) {
changes := map[string]any{}
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
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 {
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
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
containedBy := changesJson[index-1 : index]
if levelsDeep == 0 && (containedBy == "{" || containedBy == "[") {
baseLevel = true
}
}
if (opener == "{" || opener == "[") && !baseLevel {
lastOccurrence = index
lastMatched = split
levelsDeep = sIndex
}
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
}
}
}
}
// 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
}
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 any, 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 any, key string) string {
structValue := reflect.ValueOf(object)
if structValue.Kind() == reflect.Struct {
field := structValue.FieldByName(key)
id := reflection.GetStringValue(field)
return id
}
return ""
}