mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #65
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
# Utility Controllers Guide
|
||||
|
||||
This guide explains the new utility controllers that make development easier, simpler, and better.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Response Helper](#response-helper)
|
||||
2. [Pagination Helper](#pagination-helper)
|
||||
3. [Query Helper](#query-helper)
|
||||
4. [Validation Helper](#validation-helper)
|
||||
5. [Audit Log Controller](#audit-log-controller)
|
||||
6. [Batch Operations Controller](#batch-operations-controller)
|
||||
7. [Export Helper](#export-helper)
|
||||
8. [Complete Examples](#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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// GET /api/v1/articles?sort=created_at:desc
|
||||
query := controllers.QueryParser.ApplySortFromContext(
|
||||
c, db.Model(&models.Article{}), "created_at", "desc"
|
||||
)
|
||||
```
|
||||
|
||||
#### Basic Search
|
||||
|
||||
```go
|
||||
// GET /api/v1/articles?search=football
|
||||
query := controllers.QueryParser.ApplySearchFromContext(
|
||||
c, db.Model(&models.Article{}), "title", "content"
|
||||
)
|
||||
```
|
||||
|
||||
#### Fluent Chain Builder (Recommended)
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
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`:
|
||||
|
||||
```go
|
||||
// Initialize audit logger
|
||||
controllers.InitAuditLogger(dbInstance)
|
||||
|
||||
// Add to AutoMigrate
|
||||
&models.AuditLog{},
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
#### Log Actions
|
||||
|
||||
```go
|
||||
// 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)
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// Initialize in main.go or controller
|
||||
controllers.InitBatchOperations(dbInstance)
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
#### Batch Delete
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```go
|
||||
// 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)
|
||||
```go
|
||||
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)
|
||||
```go
|
||||
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
|
||||
```go
|
||||
// In main.go AutoMigrate
|
||||
&models.AuditLog{},
|
||||
```
|
||||
|
||||
### Step 2: Initialize in main.go
|
||||
```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
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
Reference in New Issue
Block a user