Files
MyClub/DOCS/NEW_UTILITY_CONTROLLERS_GUIDE.md
T
Tomas Dvorak 9ccca365b3 dev day #65
2025-10-19 18:09:28 +02:00

20 KiB

Utility Controllers Guide

This guide explains the new utility controllers that make development easier, simpler, and better.

Table of Contents

  1. Response Helper
  2. Pagination Helper
  3. Query Helper
  4. Validation Helper
  5. Audit Log Controller
  6. Batch Operations Controller
  7. Export Helper
  8. Complete Examples

Response Helper

Purpose: Standardize all API responses across your application.

Features

  • Consistent response format
  • Pre-built status code handlers
  • Support for metadata (pagination, etc.)
  • Easy error handling

Usage

import "fotbal-club/internal/controllers"

// Success response
controllers.Respond.Success(c, data, "Operation successful")

// Success with metadata (pagination)
controllers.Respond.SuccessWithMeta(c, articles, paginationMeta, "Articles retrieved")

// Created (201)
controllers.Respond.Created(c, newArticle, "Article created")

// Error responses
controllers.Respond.BadRequest(c, "Invalid input")
controllers.Respond.Unauthorized(c, "Not authenticated")
controllers.Respond.Forbidden(c, "No permission")
controllers.Respond.NotFound(c, "Article not found")
controllers.Respond.InternalError(c, "Database error")
controllers.Respond.ValidationError(c, validationErrors)

// No content (204)
controllers.Respond.NoContent(c)

Response Format

{
  "success": true,
  "message": "Articles retrieved successfully",
  "data": [...],
  "meta": {
    "page": 1,
    "page_size": 20,
    "total": 100,
    "total_pages": 5,
    "has_next": true,
    "has_prev": false
  }
}

Pagination Helper

Purpose: Add pagination to any list endpoint with one line of code.

Features

  • Auto-extracts page/page_size from query params
  • Calculates pagination metadata
  • Works with GORM queries
  • Supports preloading

Usage

// Simple pagination
query := db.Model(&models.Article{}).Where("published = ?", true)
var articles []models.Article
meta, err := controllers.Paginator.Paginate(c, query, &articles)
if err != nil {
    controllers.Respond.InternalError(c, "Failed to retrieve articles")
    return
}
controllers.Respond.SuccessWithMeta(c, articles, meta, "Success")

// Pagination with preloading
meta, err := controllers.Paginator.PaginateWithPreload(
    c, query, &articles, "Author", "Category"
)

Query Parameters

GET /api/v1/articles?page=1&page_size=20
  • page: Page number (default: 1)
  • page_size: Items per page (default: 20, max: 100)

Query Helper

Purpose: Simplify filtering, sorting, and searching in list endpoints.

Features

  • Search across multiple fields
  • Sort by any field
  • Boolean filters
  • ID filters (comma-separated)
  • Date range filters
  • Fluent chain builder

Usage

Basic Sorting

// GET /api/v1/articles?sort=created_at:desc
query := controllers.QueryParser.ApplySortFromContext(
    c, db.Model(&models.Article{}), "created_at", "desc"
)
// GET /api/v1/articles?search=football
query := controllers.QueryParser.ApplySearchFromContext(
    c, db.Model(&models.Article{}), "title", "content"
)
// GET /api/v1/articles?search=football&sort=created_at:desc&published=true&category_ids=1,2,3&from=2024-01-01
query := controllers.QueryParser.BuildQueryChain(c, db.Model(&models.Article{})).
    WithSearch("title", "content").
    WithSort("created_at", "desc").
    WithBoolFilter("published", "published").
    WithBoolFilter("featured", "featured").
    WithIDsFilter("category_ids", "category_id").
    WithDateRange("created_at").
    Build()

var articles []models.Article
meta, err := controllers.Paginator.Paginate(c, query, &articles)

Supported Query Parameters

  • search or q: Search term
  • sort: field:order (e.g., created_at:desc)
  • published: Boolean filter (true/false)
  • featured: Boolean filter (true/false)
  • category_ids: Comma-separated IDs (e.g., 1,2,3)
  • from: Start date (YYYY-MM-DD)
  • to: End date (YYYY-MM-DD)

Validation Helper

Purpose: Validate request data and return user-friendly error messages.

Features

  • Struct validation using tags
  • Custom validators (slug, color)
  • Automatic error responses
  • Input sanitization

Usage

Validate Struct

type CreateArticleRequest struct {
    Title   string `validate:"required,min=3,max=200"`
    Content string `validate:"required,min=10"`
    Slug    string `validate:"omitempty,slug"`
    Email   string `validate:"required,email"`
}

var req CreateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
    controllers.Respond.BadRequest(c, "Invalid JSON")
    return
}

// Validate and auto-respond if invalid
if !controllers.Validator.ValidateAndRespond(c, req) {
    return // Response already sent
}

// Continue with valid data...

Sanitization

// Sanitize string (trim, normalize spaces)
title := controllers.Validator.SanitizeString(req.Title)

// Sanitize email (lowercase, trim)
email := controllers.Validator.SanitizeEmail(req.Email)

// Sanitize slug
slug := controllers.Validator.SanitizeSlug(req.Slug)

Individual Validation

if !controllers.Validator.IsValidEmail(email) {
    controllers.Respond.BadRequest(c, "Invalid email")
    return
}

Validation Tags

  • required: Field is required
  • email: Valid email address
  • url: Valid URL
  • min=n: Minimum length
  • max=n: Maximum length
  • slug: Valid slug (lowercase, alphanumeric, hyphens)
  • color: Valid hex color (#RGB, #RRGGBB)
  • oneof=val1 val2: One of the specified values
  • gte=n, lte=n: Greater/less than or equal

Audit Log Controller

Purpose: Track all important actions in your application for compliance and debugging.

Features

  • Automatic user tracking
  • IP address and user agent logging
  • Before/after change tracking
  • Search and filter logs
  • Statistics dashboard

Setup

Add to main.go:

// Initialize audit logger
controllers.InitAuditLogger(dbInstance)

// Add to AutoMigrate
&models.AuditLog{},

Usage

Log Actions

// Log creation
controllers.AuditLogger.LogCreate(c, "Article", article.ID, "Article created: "+article.Title)

// Log update with changes
before := map[string]interface{}{"title": oldTitle, "published": oldPublished}
after := map[string]interface{}{"title": newTitle, "published": newPublished}
controllers.AuditLogger.LogUpdate(c, "Article", article.ID, "Article updated", before, after)

// Log deletion
controllers.AuditLogger.LogDelete(c, "Article", articleID, "Article deleted: "+title)

// Log login
controllers.AuditLogger.LogLogin(c, userID, true) // success=true

// Log custom action
controllers.AuditLogger.LogEntry(c, "EXPORT", "Article", nil, "Exported articles to CSV", nil)

API Endpoints (Admin Only)

// Add to routes/routes.go admin group:
admin.GET("/audit-logs", auditLogController.GetAuditLogs)
admin.GET("/audit-logs/:id", auditLogController.GetAuditLogByID)
admin.GET("/audit-logs/entity/:entity_type/:entity_id", auditLogController.GetEntityAuditHistory)
admin.GET("/audit-logs/user/:user_id", auditLogController.GetUserActivityLog)
admin.GET("/audit-logs/stats", auditLogController.GetAuditStats)
admin.POST("/audit-logs/cleanup", auditLogController.CleanupOldLogs)

Query Audit Logs

GET /api/v1/admin/audit-logs?action=CREATE&entity_type=Article&user_id=1&from=2024-01-01&page=1

Batch Operations Controller

Purpose: Perform bulk operations efficiently.

Features

  • Batch delete
  • Batch update
  • Batch publish/unpublish
  • Batch reorder
  • Detailed success/failure reporting

Setup

// Initialize in main.go or controller
controllers.InitBatchOperations(dbInstance)

Usage

Batch Delete

// POST /api/v1/articles/batch-delete
// Body: {"ids": [1, 2, 3, 4, 5]}
func (ac *ArticleController) BatchDeleteArticles(c *gin.Context) {
    controllers.BatchOps.BatchDelete(c, &models.Article{}, "articles")
}

Batch Update

// POST /api/v1/articles/batch-update
// Body: {"ids": [1, 2, 3], "fields": {"published": true, "featured": false}}
func (ac *ArticleController) BatchUpdateArticles(c *gin.Context) {
    allowedFields := []string{"published", "featured", "category_id"}
    controllers.BatchOps.BatchUpdate(c, &models.Article{}, "articles", allowedFields)
}

Batch Publish/Unpublish

// POST /api/v1/articles/batch-publish
// Body: {"ids": [1, 2, 3]}
func (ac *ArticleController) BatchPublishArticles(c *gin.Context) {
    controllers.BatchOps.BatchPublish(c, &models.Article{}, "articles", true)
}

Batch Reorder

// POST /api/v1/navigation/batch-reorder
// Body: {"orders": [{"id": 1, "order": 3}, {"id": 2, "order": 1}]}
func (nc *NavigationController) BatchReorderItems(c *gin.Context) {
    controllers.BatchOps.BatchReorder(c, &models.NavigationItem{}, "navigation_items")
}

Response Format

{
  "success": true,
  "data": {
    "success": true,
    "total_items": 5,
    "success_count": 5,
    "failure_count": 0,
    "errors": []
  },
  "message": "Successfully deleted 5 items"
}

Export Helper

Purpose: Export data to CSV or JSON format.

Usage

Export to CSV

func (ac *ArticleController) ExportArticlesToCSV(c *gin.Context) {
    var articles []models.Article
    if err := db.Find(&articles).Error; err != nil {
        controllers.Respond.InternalError(c, "Failed to retrieve articles")
        return
    }

    headers := []string{"ID", "Title", "Published", "Created At"}
    filename := fmt.Sprintf("articles_%s.csv", time.Now().Format("20060102"))
    
    if err := controllers.Exporter.ExportToCSV(c, articles, filename, headers); err != nil {
        controllers.Respond.InternalError(c, "Export failed")
        return
    }
}

Export to JSON

func (ac *ArticleController) ExportArticlesToJSON(c *gin.Context) {
    var articles []models.Article
    if err := db.Find(&articles).Error; err != nil {
        controllers.Respond.InternalError(c, "Failed to retrieve articles")
        return
    }

    filename := fmt.Sprintf("articles_%s.json", time.Now().Format("20060102"))
    
    if err := controllers.Exporter.ExportToJSON(c, articles, filename); err != nil {
        controllers.Respond.InternalError(c, "Export failed")
        return
    }
}

Complete Examples

Example 1: Simple CRUD Controller

package controllers

import (
    "fotbal-club/internal/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

type SimpleArticleController struct {
    DB *gorm.DB
}

// List articles with search, filter, sort, and pagination
func (sac *SimpleArticleController) List(c *gin.Context) {
    query := QueryParser.BuildQueryChain(c, sac.DB.Model(&models.Article{})).
        WithSearch("title", "content").
        WithSort("created_at", "desc").
        WithBoolFilter("published", "published").
        Build()

    var articles []models.Article
    meta, err := Paginator.PaginateWithPreload(c, query, &articles, "Author", "Category")
    if err != nil {
        Respond.InternalError(c, "Failed to retrieve articles")
        return
    }

    Respond.SuccessWithMeta(c, articles, meta, "Articles retrieved successfully")
}

// Get single article
func (sac *SimpleArticleController) Get(c *gin.Context) {
    id := c.Param("id")

    var article models.Article
    if err := sac.DB.Preload("Author").Preload("Category").First(&article, id).Error; err != nil {
        if err == gorm.ErrRecordNotFound {
            Respond.NotFound(c, "Article not found")
            return
        }
        Respond.InternalError(c, "Failed to retrieve article")
        return
    }

    Respond.Success(c, article, "Article retrieved successfully")
}

// Create article
func (sac *SimpleArticleController) Create(c *gin.Context) {
    var req struct {
        Title   string `json:"title" validate:"required,min=3,max=200"`
        Content string `json:"content" validate:"required,min=10"`
        Slug    string `json:"slug" validate:"omitempty,slug"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        Respond.BadRequest(c, "Invalid JSON")
        return
    }

    if !Validator.ValidateAndRespond(c, req) {
        return
    }

    article := models.Article{
        Title:   Validator.SanitizeString(req.Title),
        Content: req.Content,
        Slug:    Validator.SanitizeSlug(req.Slug),
    }

    if err := sac.DB.Create(&article).Error; err != nil {
        Respond.InternalError(c, "Failed to create article")
        return
    }

    AuditLogger.LogCreate(c, "Article", article.ID, "Article created: "+article.Title)
    Respond.Created(c, article, "Article created successfully")
}

// Update article
func (sac *SimpleArticleController) Update(c *gin.Context) {
    id := c.Param("id")

    var article models.Article
    if err := sac.DB.First(&article, id).Error; err != nil {
        if err == gorm.ErrRecordNotFound {
            Respond.NotFound(c, "Article not found")
            return
        }
        Respond.InternalError(c, "Failed to retrieve article")
        return
    }

    oldTitle := article.Title

    var req struct {
        Title string `json:"title" validate:"omitempty,min=3,max=200"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        Respond.BadRequest(c, "Invalid JSON")
        return
    }

    if !Validator.ValidateAndRespond(c, req) {
        return
    }

    if req.Title != "" {
        article.Title = Validator.SanitizeString(req.Title)
    }

    if err := sac.DB.Save(&article).Error; err != nil {
        Respond.InternalError(c, "Failed to update article")
        return
    }

    AuditLogger.LogUpdate(c, "Article", article.ID, "Article updated",
        map[string]interface{}{"title": oldTitle},
        map[string]interface{}{"title": article.Title})

    Respond.Success(c, article, "Article updated successfully")
}

// Delete article
func (sac *SimpleArticleController) Delete(c *gin.Context) {
    id := c.Param("id")

    var article models.Article
    if err := sac.DB.First(&article, id).Error; err != nil {
        if err == gorm.ErrRecordNotFound {
            Respond.NotFound(c, "Article not found")
            return
        }
        Respond.InternalError(c, "Failed to retrieve article")
        return
    }

    title := article.Title
    articleID := article.ID

    if err := sac.DB.Delete(&article).Error; err != nil {
        Respond.InternalError(c, "Failed to delete article")
        return
    }

    AuditLogger.LogDelete(c, "Article", articleID, "Article deleted: "+title)
    Respond.NoContent(c)
}

Example 2: Route Registration

// In routes/routes.go

func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
    // Initialize helpers
    controllers.InitAuditLogger(db)
    controllers.InitBatchOperations(db)

    // Article routes with new utilities
    articles := api.Group("/articles")
    {
        articleCtrl := &controllers.SimpleArticleController{DB: db}
        
        // Public routes
        articles.GET("", articleCtrl.List)
        articles.GET("/:id", articleCtrl.Get)
        
        // Protected routes
        protected := articles.Group("")
        protected.Use(middleware.JWTAuth(db))
        {
            protected.POST("", articleCtrl.Create)
            protected.PUT("/:id", articleCtrl.Update)
            protected.DELETE("/:id", articleCtrl.Delete)
            
            // Batch operations
            protected.POST("/batch-delete", articleCtrl.BatchDelete)
            protected.POST("/batch-publish", articleCtrl.BatchPublish)
            
            // Export
            protected.GET("/export/csv", articleCtrl.ExportCSV)
            protected.GET("/export/json", articleCtrl.ExportJSON)
        }
    }
    
    // Audit logs (admin only)
    auditLogCtrl := controllers.NewAuditLogController(db)
    admin := api.Group("/admin")
    admin.Use(middleware.JWTAuth(db), middleware.RoleAuth("admin"))
    {
        admin.GET("/audit-logs", auditLogCtrl.GetAuditLogs)
        admin.GET("/audit-logs/:id", auditLogCtrl.GetAuditLogByID)
        admin.GET("/audit-logs/stats", auditLogCtrl.GetAuditStats)
        admin.POST("/audit-logs/cleanup", auditLogCtrl.CleanupOldLogs)
    }
}

Benefits

Before (Old Way)

func GetArticles(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
    
    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }
    
    offset := (page - 1) * pageSize
    
    var articles []models.Article
    var total int64
    
    query := db.Model(&models.Article{})
    query.Count(&total)
    query.Offset(offset).Limit(pageSize).Find(&articles)
    
    totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
    
    c.JSON(200, gin.H{
        "data": articles,
        "page": page,
        "page_size": pageSize,
        "total": total,
        "total_pages": totalPages,
    })
}

After (New Way)

func GetArticles(c *gin.Context) {
    query := QueryParser.BuildQueryChain(c, db.Model(&models.Article{})).
        WithSearch("title", "content").
        WithSort("created_at", "desc").
        Build()

    var articles []models.Article
    meta, _ := Paginator.Paginate(c, query, &articles)
    Respond.SuccessWithMeta(c, articles, meta, "Success")
}

Comparison

  • Lines of code: 24 → 7 (70% reduction)
  • Consistency: Standardized across all endpoints
  • Features: Added search, sorting, filtering
  • Error handling: Built-in
  • Maintainability: Single source of truth

Migration Guide

Step 1: Add Model Migration

// In main.go AutoMigrate
&models.AuditLog{},

Step 2: Initialize in main.go

// After database initialization
controllers.InitAuditLogger(dbInstance)
controllers.InitBatchOperations(dbInstance)

Step 3: Update Existing Controllers

Replace manual pagination, filtering, and response code with the new utilities.

Step 4: Add go-playground/validator Dependency

go get github.com/go-playground/validator/v10

Best Practices

  1. Always use standardized responses - Use Respond.* methods
  2. Log important actions - Use AuditLogger for CREATE, UPDATE, DELETE
  3. Validate inputs - Use Validator.ValidateAndRespond
  4. Sanitize user input - Use Validator.Sanitize* methods
  5. Use query chains - Use QueryParser.BuildQueryChain for complex queries
  6. Paginate large lists - Always use Paginator for list endpoints
  7. Batch operations - Use BatchOps for bulk actions
  8. Export functionality - Use Exporter for CSV/JSON exports

Testing

# Test pagination
curl "http://localhost:8080/api/v1/articles?page=1&page_size=10"

# Test search
curl "http://localhost:8080/api/v1/articles?search=football"

# Test sorting
curl "http://localhost:8080/api/v1/articles?sort=created_at:desc"

# Test combined
curl "http://localhost:8080/api/v1/articles?search=football&sort=created_at:desc&published=true&page=1"

# Test batch delete
curl -X POST "http://localhost:8080/api/v1/articles/batch-delete" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [1, 2, 3]}'

# Test audit logs
curl "http://localhost:8080/api/v1/admin/audit-logs?action=CREATE&entity_type=Article"

Conclusion

These utility controllers dramatically reduce boilerplate code, improve consistency, and make your codebase more maintainable. They follow Go best practices and provide a solid foundation for rapid development.

Key Improvements:

  • 70% less code for common operations
  • Consistent API responses
  • Built-in validation and sanitization
  • Comprehensive audit logging
  • Efficient batch operations
  • Easy data export
  • Better error handling
  • Improved developer experience