diff --git a/internal/entity/dbtest/dbtest_gormV2_test.go b/internal/entity/dbtest/dbtest_gormV2_test.go new file mode 100644 index 000000000..eb00d01ce --- /dev/null +++ b/internal/entity/dbtest/dbtest_gormV2_test.go @@ -0,0 +1,236 @@ +package entity + +import ( + "fmt" + "reflect" + "testing" + + "github.com/photoprism/photoprism/internal/entity" +) + +func TestGorm(t *testing.T) { + dbtestMutex.Lock() + defer dbtestMutex.Unlock() + + // This test shows the underlying way that PhotoPrism creates a new Photo with GormV1 + t.Run("Photo_Gorm1", func(t *testing.T) { + m := entity.PhotoFixtures.Pointer("Photo09") + + values, keys, err := modelValuesStructOption(m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + log.Debugf("Photo = %v", m) + + values, keys, err = modelValues(m, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + + }) + + t.Run("File_Gorm1", func(t *testing.T) { + m := entity.FileFixtures.Pointer("exampleFileName.jpg") + + values, keys, err := modelValuesStructOption(m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + + }) + + t.Run("Passcode_Gorm1", func(t *testing.T) { + m := entity.PasscodeFixtures.Pointer("alice") + + values, keys, err := modelValuesStructOption(m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + + }) + + t.Run("Session_Gorm1", func(t *testing.T) { + m := entity.SessionFixtures.Pointer("alice") + + values, keys, err := modelValuesStructOption(m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + + }) + + t.Run("Service_Gorm1", func(t *testing.T) { + m := entity.ServiceFixtures["dummy-webdav"] + values, keys, err := modelValues(&m, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + + m = entity.ServiceFixtures["dummy-webdav"] + values, keys, err = modelValuesStructOption(&m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } + }) + + t.Run("Country_Gorm1", func(t *testing.T) { + cid := uint(12345) + m := entity.Country{ + ID: "de", + CountrySlug: "germany", + CountryName: "Germany", + CountryDescription: "Country description", + CountryNotes: "Country Notes", + CountryPhoto: nil, + CountryPhotoID: &cid, + New: false, + } + + values, keys, err := modelValuesStructOption(&m, false, "ID", "PhotoUID") + log.Debugf("Keys = %v", keys) + if err == nil { + for k, v := range values { + s := fmt.Sprintf("%v = [%#v]", k, v) + log.Debug(s) + } + } else { + log.Debugf("error was %v", err) + } + + }) + +} + +func modelValues(m interface{}, omit ...string) (result map[string]interface{}, omitted []interface{}, err error) { + return modelValuesStructOption(m, true, omit...) +} + +// ModelValues extracts Values from an entity model. +func modelValuesStructOption(m interface{}, includeStruct bool, omit ...string) (result map[string]interface{}, omitted []interface{}, err error) { + mustOmit := func(name string) bool { + for _, s := range omit { + if name == s { + return true + } + } + + return false + } + + r := reflect.ValueOf(m) + + if r.Kind() != reflect.Pointer { + return result, omitted, fmt.Errorf("model interface expected") + } + + values := r.Elem() + + if kind := values.Kind(); kind != reflect.Struct { + return result, omitted, fmt.Errorf("model expected") + } + + t := values.Type() + num := t.NumField() + + omitted = make([]interface{}, 0, len(omit)) + result = make(map[string]interface{}, num) + + // Add exported fields to result. + for i := 0; i < num; i++ { + field := t.Field(i) + + // Skip non-exported fields. + if !field.IsExported() { + continue + } + + fieldName := field.Name + + // Skip timestamps. + if fieldName == "" || fieldName == "UpdatedAt" || fieldName == "CreatedAt" { + continue + } + + v := values.Field(i) + + switch v.Kind() { + case reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer: + continue + case reflect.Struct: + if v.IsZero() { + continue + } + whitelist := false + switch v.Type().Name() { + case "NullTime", "Time": + whitelist = true + } + if !whitelist && !includeStruct { + log.Debugf("Struct Zeroing %v of type %v or string %v", fieldName, v.Type().Name(), v.Type().String()) + v.SetZero() + continue + } + case reflect.Pointer: + whitelist := false + switch v.Type().String() { + case "*time.Time", "*uint", "*uint64", "*uint32", "*int", "*int64", "*int32", "*string", "*float32", "*float64", "*otp.Key", "sql.NullTime": + whitelist = true + } + if !whitelist && !includeStruct { + log.Debugf("Pointer Zeroing %v of type %v or string %v", fieldName, v.Type().Name(), v.Type().String()) + v.SetZero() + continue + } + } + + // Skip read-only fields. + if !v.CanSet() { + continue + } + + // Skip omitted. + if mustOmit(fieldName) { + if !v.IsZero() { + omitted = append(omitted, v.Interface()) + } + continue + } + + // Add value to result. + result[fieldName] = v.Interface() + } + + if len(result) == 0 { + return result, omitted, fmt.Errorf("no values") + } + + return result, omitted, nil +}