Skip to main content

Hooks & Events

DEVELOPER Advanced

Use lifecycle hooks and events to extend EZ-Console functionality.

Overview​

EZ-Console’s authorization stack exposes user change and user role change hooks on server.Service (backed by pkg/service). You need a live Service value to register themβ€”the stock NewCommandServer binary builds that service internally, so wiring hooks from an external app may require a bootstrap that matches your version (for example tests, a fork, or future public extension points). The APIs below are accurate; where you call them depends on how you host the server.

User Change Hooks​

Registering User Change Hooks​

Called when user records change (implementation details vary by action). Example shape once you hold svc server.Service:

import (
"context"
"github.com/sven-victor/ez-console/pkg/model"
)

svc.RegisterUserChangeHook(func(ctx context.Context, user *model.User, action string) error {
switch action {
case "create":
sendWelcomeEmail(user)
case "update":
syncUserToExternalSystem(user)
case "delete":
cleanupUserData(user)
}
return nil
})

User Role Change Hooks​

svc.RegisterUserRoleChangeHook(func(ctx context.Context, userID string, roleIDs []string) error {
updatePermissionsCache(userID, roleIDs)
notifyRoleChange(userID, roleIDs)
return nil
})

Model Hooks (GORM)​

BeforeCreate Hook​

Execute code before creating a record:

type Product struct {
ID string `gorm:"primaryKey"`
Name string
Price float64
}

func (p *Product) BeforeCreate(tx *gorm.DB) error {
// Generate ID if not set
if p.ID == "" {
p.ID = uuid.New().String()
}

// Validate price
if p.Price < 0 {
return fmt.Errorf("price cannot be negative")
}

// Set timestamps
p.CreatedAt = time.Now()
p.UpdatedAt = time.Now()

return nil
}

AfterCreate Hook​

Execute code after creating a record:

func (p *Product) AfterCreate(tx *gorm.DB) error {
// Publish event
events.Publish("product.created", p)

// Send notification
sendProductCreatedNotification(p)

// Update cache
cache.Set("product:"+p.ID, p, 5*time.Minute)

return nil
}

BeforeUpdate Hook​

Execute code before updating a record:

func (p *Product) BeforeUpdate(tx *gorm.DB) error {
// Track changes
if tx.Statement.Changed("Price") {
logPriceChange(p.ID, p.Price)
}

// Update timestamp
p.UpdatedAt = time.Now()

return nil
}

AfterUpdate Hook​

Execute code after updating a record:

func (p *Product) AfterUpdate(tx *gorm.DB) error {
// Invalidate cache
cache.Delete("product:" + p.ID)

// Publish event
events.Publish("product.updated", p)

return nil
}

BeforeDelete Hook​

Execute code before deleting a record:

func (p *Product) BeforeDelete(tx *gorm.DB) error {
// Check if can be deleted
if p.HasOrders() {
return fmt.Errorf("cannot delete product with existing orders")
}

return nil
}

AfterDelete Hook​

Execute code after deleting a record:

func (p *Product) AfterDelete(tx *gorm.DB) error {
// Cleanup related data
cleanupProductImages(p.ID)
cleanupProductReviews(p.ID)

// Publish event
events.Publish("product.deleted", p.ID)

return nil
}

Event System​

Publishing Events​

Publish custom events:

import "github.com/sven-victor/ez-console/pkg/events"

func (s *ProductService) CreateProduct(ctx context.Context, product *Product) error {
// Create product
if err := s.db.Create(product).Error; err != nil {
return err
}

// Publish event
events.Publish("product.created", product)

return nil
}

Subscribing to Events​

Subscribe to events:

func init() {
events.Subscribe("product.created", func(data interface{}) {
product := data.(*Product)

// Send notification
sendProductCreatedNotification(product)

// Update search index
indexProduct(product)
})

events.Subscribe("product.updated", func(data interface{}) {
product := data.(*Product)

// Update cache
cache.Set("product:"+product.ID, product, 5*time.Minute)
})
}

Audit Logging Hooks​

Using Audit Service​

The framework provides an audit service for automatic logging:

func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}

var product *model.Product
err := c.svc.StartAudit(ctx, "", func(auditLog *model.AuditLog) error {
product, err = c.productService.Create(ctx.Request.Context(), req)
auditLog.ResourceType = "product"
auditLog.Action = "create"
auditLog.ResourceID = product.ID
return err
})

if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to create", err))
return
}

util.RespondWithSuccess(ctx, http.StatusCreated, product)
}

Best Practices​

1. Keep Hooks Simple​

// βœ… Good: Simple hook
func (p *Product) BeforeCreate(tx *gorm.DB) error {
if p.ID == "" {
p.ID = uuid.New().String()
}
return nil
}

// ❌ Bad: Complex business logic in hook
func (p *Product) BeforeCreate(tx *gorm.DB) error {
// Too much logic here
// Should be in service layer
}

2. Handle Errors Properly​

// βœ… Good: Return errors
func (p *Product) BeforeCreate(tx *gorm.DB) error {
if p.Price < 0 {
return fmt.Errorf("invalid price")
}
return nil
}

// ❌ Bad: Ignore errors
func (p *Product) BeforeCreate(tx *gorm.DB) error {
// Error handling missing
return nil
}

3. Use Context​

// βœ… Good: Use context
func hook(ctx context.Context, user *model.User, action string) error {
// Use context for cancellation, timeouts, etc.
return nil
}

Need help? Ask in GitHub Discussions.