Your Question

How to handle related models inside a plugin?

The document you expected this should be explained

https://gorm.io/docs/write_plugins.html

Expected answer

I did a custom plugin to handle timestamppb.Timestamp protobuf type:

package timestamppb

import (
    "reflect"
    "time"

    "google.golang.org/protobuf/types/known/timestamppb"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
)

type TimestamppbPlugin struct{}

func (p *TimestamppbPlugin) Name() string {
    return "timestamppb"
}

func (p *TimestamppbPlugin) Initialize(db *gorm.DB) (err error) {
    // Soft deleteAt when type is timestamppb
    db.Callback().Delete().Before("gorm:before_delete").Register(p.Name(), p.BeforeDelete)
    // Set createAt when type is timestamppb
    db.Callback().Create().Before("gorm:before_create").Register(p.Name(), p.BeforeCreate)
    // Set updateAt when type is timestamppb
    db.Callback().Update().Before("gorm:update").Register(p.Name(), p.BeforeUpdate)
    // Add where clause
    db.Callback().Query().Before("gorm:query").Register(p.Name(), p.BeforeQuery)
    return
}

func (p *TimestamppbPlugin) BeforeCreate(db *gorm.DB) {
    p.updateFields("AUTOCREATETIMESTAMPPB", db)
    p.updateFields("AUTOUPDATETIMESTAMPPB", db)
}

func (p *TimestamppbPlugin) BeforeUpdate(db *gorm.DB) {
    p.updateFields("AUTOUPDATETIMESTAMPPB", db)
}

func (p *TimestamppbPlugin) BeforeQuery(db *gorm.DB) {

    if db.Statement.Schema == nil || db.Statement.Unscoped {
        return
    }
    if _, ok := db.Statement.Schema.FieldsByName["DeletedAt"]; !ok {
        return
    }
    deletedAtField := db.Statement.Schema.FieldsByName["DeletedAt"]

    if deletedAtField.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
        // Modify query to add deleteAt is NULL
        db = db.Where(db.Statement.Table + "." + deletedAtField.DBName + " IS NULL")
    }

}

func (p *TimestamppbPlugin) BeforeDelete(db *gorm.DB) {

    if db.Statement.Schema == nil || db.Statement.Unscoped {
        return
    }
    if _, ok := db.Statement.Schema.FieldsByName["DeletedAt"]; !ok {
        return
    }
    var set clause.Set
    deletedAtField := db.Statement.Schema.FieldsByName["DeletedAt"]

    if deletedAtField.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
        // Modify query to update instead of delete the record
        timeNow := time.Now()
        now := timestamppb.New(timeNow)
        set = append(clause.Set{{Column: clause.Column{Name: deletedAtField.DBName}, Value: timeNow}}, set...)
        db.Statement.AddClause(set)
        db.Statement.SetColumn(deletedAtField.DBName, now, true)

        db.Statement.AddClauseIfNotExists(clause.Update{})
        db.Statement.Build(db.Statement.DB.Callback().Update().Clauses...)
    }

}

// Update field value
func (p *TimestamppbPlugin) updateFields(trigger string, db *gorm.DB) (err error) {

    if db.Statement.Schema != nil {
        for _, field := range db.Statement.Schema.Fields {
            if db.Statement.ReflectValue.Kind() == reflect.Struct {

                // Update field
                if field.TagSettings[trigger] != "" && field.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
                    now := timestamppb.New(time.Now())
                    fieldValue := db.Statement.ReflectValue.FieldByName(field.Name)

                    if fieldValue.CanSet() {
                        db.Statement.SetColumn(field.Name, now)
                    }
                }
            }
        }

    }

    return nil
}

Now is working only for main model, is there a way to handle has many has one relations as well?

Comment From: asfwebmaster

I have manage to fix it, the problem was that the related model is slice, not a struct here is updated working version of the plugin:

``` package timestamppb

import ( "reflect" "time"

"google.golang.org/protobuf/types/known/timestamppb"
"gorm.io/gorm"
"gorm.io/gorm/clause"

)

type TimestamppbPlugin struct{}

func (p *TimestamppbPlugin) Name() string { return "timestamppb" }

func (p TimestamppbPlugin) Initialize(db gorm.DB) (err error) { // Set createAt when type is timestamppb db.Callback().Create().Before("").Register(p.Name(), p.BeforeCreate) // Set updateAt when type is timestamppb db.Callback().Update().Before("").Register(p.Name(), p.BeforeUpdate) // Soft deleteAt when type is timestamppb db.Callback().Delete().Before("gorm:before_delete").Register(p.Name(), p.BeforeDelete) // Add where clause db.Callback().Query().Before("gorm:query").Register(p.Name(), p.BeforeQuery) return }

func (p TimestamppbPlugin) BeforeCreate(db gorm.DB) { p.updateFields("AUTOCREATETIMESTAMPPB", db) }

func (p TimestamppbPlugin) BeforeUpdate(db gorm.DB) { p.updateFields("AUTOUPDATETIMESTAMPPB", db) }

func (p TimestamppbPlugin) BeforeQuery(db gorm.DB) {

if db.Statement.Schema == nil || db.Statement.Unscoped {
    return
}
if _, ok := db.Statement.Schema.FieldsByName["DeletedAt"]; !ok {
    return
}
deletedAtField := db.Statement.Schema.FieldsByName["DeletedAt"]

if deletedAtField.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
    // Modify query to add deleteAt is NULL
    db = db.Where(db.Statement.Table + "." + deletedAtField.DBName + " IS NULL")
}

}

func (p TimestamppbPlugin) BeforeDelete(db gorm.DB) {

if db.Statement.Schema == nil || db.Statement.Unscoped {
    return
}
if _, ok := db.Statement.Schema.FieldsByName["DeletedAt"]; !ok {
    return
}
var set clause.Set
deletedAtField := db.Statement.Schema.FieldsByName["DeletedAt"]

if deletedAtField.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
    // Modify query to update instead of delete the record
    timeNow := time.Now()
    now := timestamppb.New(timeNow)
    set = append(clause.Set{{Column: clause.Column{Name: deletedAtField.DBName}, Value: timeNow}}, set...)
    db.Statement.AddClause(set)
    db.Statement.SetColumn(deletedAtField.DBName, now, true)

    db.Statement.AddClauseIfNotExists(clause.Update{})
    db.Statement.Build(db.Statement.DB.Callback().Update().Clauses...)
}

}

// Update field value func (p TimestamppbPlugin) updateFields(trigger string, db gorm.DB) (err error) {

if db.Statement.Schema != nil {
    now := timestamppb.New(time.Now())
    for _, field := range db.Statement.Schema.Fields {
        switch db.Statement.ReflectValue.Kind() {
        case reflect.Slice, reflect.Array:
            for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
                if field.TagSettings[trigger] != "" && field.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
                    field.Set(db.Statement.Context, db.Statement.ReflectValue.Index(i), now)
                }
            }
        case reflect.Struct:
            if field.TagSettings[trigger] != "" && field.FieldType == reflect.TypeOf(&timestamppb.Timestamp{}) {
                // Set value to field
                err := field.Set(db.Statement.Context, db.Statement.ReflectValue, now)
                return err
            }
        }
    }
}

return nil

}

```