Files
antrean-anjungan/tools/general/generate-handler.go

1591 lines
48 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// HandlerData contains template data for handler generation
type HandlerData struct {
Name string
NameLower string
NamePlural string
Category string
CategoryPath string
ModuleName string
TableName string
HasGet bool
HasPost bool
HasPut bool
HasDelete bool
HasStats bool
HasDynamic bool
HasSearch bool
HasFilter bool
HasPagination bool
Timestamp string
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run generate-handler.go [category/]entity [methods]")
fmt.Println("Examples:")
fmt.Println(" go run generate-handler.go product get post put delete")
fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search")
os.Exit(1)
}
// Parse entity path (could be "entity" or "category/entity")
entityPath := os.Args[1]
methods := []string{}
if len(os.Args) > 2 {
methods = os.Args[2:]
} else {
// Default methods with advanced features
methods = []string{"get", "post", "put", "delete", "dynamic", "search"}
}
// Parse category and entity
var category, entityName string
if strings.Contains(entityPath, "/") {
parts := strings.Split(entityPath, "/")
if len(parts) != 2 {
fmt.Println("❌ Error: Invalid path format. Use 'category/entity' or just 'entity'")
os.Exit(1)
}
category = parts[0]
entityName = parts[1]
} else {
category = ""
entityName = entityPath
}
// Format names
entityName = strings.Title(entityName) // PascalCase entity name
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
// Table name: include category if exists
var tableName string
if category != "" {
tableName = "data_" + category + "_" + entityLower
} else {
tableName = "data_" + entityLower
}
data := HandlerData{
Name: entityName,
NameLower: entityLower,
NamePlural: entityPlural,
Category: category,
CategoryPath: category,
ModuleName: "api-service",
TableName: tableName,
HasPagination: true,
HasFilter: true,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
// Set methods based on arguments
for _, m := range methods {
switch strings.ToLower(m) {
case "get":
data.HasGet = true
case "post":
data.HasPost = true
case "put":
data.HasPut = true
case "delete":
data.HasDelete = true
case "stats":
data.HasStats = true
case "dynamic":
data.HasDynamic = true
case "search":
data.HasSearch = true
}
}
// Always add stats if we have get
if data.HasGet {
data.HasStats = true
}
// Create directories with improved logic
var handlerDir, modelDir string
if category != "" {
handlerDir = filepath.Join("internal", "handlers", category)
modelDir = filepath.Join("internal", "models", category)
} else {
handlerDir = filepath.Join("internal", "handlers")
modelDir = filepath.Join("internal", "models")
}
// Create directories
for _, d := range []string{handlerDir, modelDir} {
if err := os.MkdirAll(d, 0755); err != nil {
panic(err)
}
}
// Generate files
generateHandlerFile(data, handlerDir)
generateModelFile(data, modelDir)
updateRoutesFile(data)
fmt.Printf("✅ Successfully generated handler: %s\n", entityName)
if category != "" {
fmt.Printf("📁 Category: %s\n", category)
}
fmt.Printf("📁 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go"))
fmt.Printf("📁 Model: %s\n", filepath.Join(modelDir, entityLower+".go"))
}
// ================= HANDLER GENERATION =====================
func generateHandlerFile(data HandlerData, handlerDir string) {
// Build import path based on category
var modelsImportPath string
if data.Category != "" {
modelsImportPath = data.ModuleName + "/internal/models/" + data.Category
} else {
modelsImportPath = data.ModuleName + "/internal/models"
}
handlerContent := `package handlers
import (
"` + data.ModuleName + `/internal/config"
"` + data.ModuleName + `/internal/database"
models "` + data.ModuleName + `/internal/models"
models` + data.NameLower + ` "` + modelsImportPath + `"`
// Add conditional imports for dynamic and search functionality
if data.HasDynamic || data.HasSearch {
handlerContent += `
utils "` + data.ModuleName + `/internal/utils/filters"`
}
handlerContent += `
"` + data.ModuleName + `/internal/utils/validation"
"context"
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
// Initialize the database connection and validator
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("` + data.NameLower + `_status", validate` + data.Name + `Status)
if db == nil {
log.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for ` + data.NameLower + ` status
func validate` + data.Name + `Status(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// ` + data.Name + `Handler handles ` + data.NameLower + ` services
type ` + data.Name + `Handler struct {
db database.Service
}
// New` + data.Name + `Handler creates a new ` + data.Name + `Handler
func New` + data.Name + `Handler() *` + data.Name + `Handler {
return &` + data.Name + `Handler{
db: db,
}
}`
// Add methods
if data.HasGet {
handlerContent += generateGetMethods(data)
}
if data.HasDynamic {
handlerContent += generateDynamicMethod(data)
}
if data.HasSearch {
handlerContent += generateSearchMethod(data)
}
if data.HasPost {
handlerContent += generateCreateMethod(data)
}
if data.HasPut {
handlerContent += generateUpdateMethod(data)
}
if data.HasDelete {
handlerContent += generateDeleteMethod(data)
}
if data.HasStats {
handlerContent += generateStatsMethod(data)
}
// Add helper methods
handlerContent += generateHelperMethods(data)
writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent)
}
func generateGetMethods(data HandlerData) string {
return `
// Get` + data.Name + ` godoc
// @Summary Get ` + data.NameLower + ` with pagination and optional aggregation
// @Description Returns a paginated list of ` + data.NamePlural + ` with optional summary statistics
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Param include_summary query bool false "Include aggregation summary" default(false)
// @Param status query string false "Filter by status"
// @Param search query string false "Search in multiple fields"
// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NamePlural + ` [get]
func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) {
// Parse pagination parameters
limit, offset, err := h.parsePaginationParams(c)
if err != nil {
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
return
}
// Parse filter parameters
filter := h.parseFilterParams(c)
includeAggregation := c.Query("include_summary") == "true"
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute concurrent operations
var (
items []models` + data.NameLower + `.` + data.Name + `
total int
aggregateData *models.AggregateData
wg sync.WaitGroup
errChan = make(chan error, 3)
mu sync.Mutex
)
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
mu.Lock()
errChan <- fmt.Errorf("failed to get total count: %w", err)
mu.Unlock()
}
}()
// Fetch main data
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to fetch data: %w", err)
} else {
items = result
}
mu.Unlock()
}()
// Fetch aggregation data if requested
if includeAggregation {
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.getAggregateData(ctx, dbConn, filter)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
} else {
aggregateData = result
}
mu.Unlock()
}()
}
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
return
}
}
// Build response
meta := h.calculateMeta(limit, offset, total)
response := models` + data.NameLower + `.` + data.Name + `GetResponse{
Message: "Data ` + data.NameLower + ` berhasil diambil",
Data: items,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// Get` + data.Name + `ByID godoc
// @Summary Get ` + data.Name + ` by ID
// @Description Returns a single ` + data.NameLower + ` by ID
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param id path string true "` + data.Name + ` ID (UUID)"
// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NameLower + `/{id} [get]
func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.get` + data.Name + `ByID(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError)
}
return
}
response := models` + data.NameLower + `.` + data.Name + `GetByIDResponse{
Message: "` + data.Name + ` details retrieved successfully",
Data: item,
}
c.JSON(http.StatusOK, response)
}`
}
func generateDynamicMethod(data HandlerData) string {
return `
// Get` + data.Name + `Dynamic godoc
// @Summary Get ` + data.NameLower + ` with dynamic filtering
// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param fields query string false "Fields to select (e.g., fields=*.*)"
// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[name][_eq]=value)"
// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)"
// @Param limit query int false "Limit" default(10)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `GetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NamePlural + `/dynamic [get]
func (h *` + data.Name + `Handler) Get` + data.Name + `Dynamic(c *gin.Context) {
// Parse query parameters
parser := utils.NewQueryParser().SetLimits(10, 100)
dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query())
if err != nil {
h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest)
return
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute query with dynamic filtering
items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total)
response := models` + data.NameLower + `.` + data.Name + `GetResponse{
Message: "Data ` + data.NameLower + ` berhasil diambil",
Data: items,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}`
}
func generateSearchMethod(data HandlerData) string {
return `
// Search` + data.Name + `Advanced provides advanced search capabilities
func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) {
// Parse complex search parameters
searchQuery := c.Query("q")
if searchQuery == "" {
h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest)
return
}
// Build dynamic query for search
query := utils.DynamicQuery{
Fields: []string{"*"},
Filters: []utils.FilterGroup{{
Filters: []utils.DynamicFilter{
{
Column: "status",
Operator: utils.OpNotEqual,
Value: "deleted",
},
{
Column: "name",
Operator: utils.OpContains,
Value: searchQuery,
LogicOp: "OR",
},
},
LogicOp: "AND",
}},
Sort: []utils.SortField{{
Column: "date_created",
Order: "DESC",
}},
Limit: 20,
Offset: 0,
}
// Parse pagination if provided
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
query.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
query.Offset = o
}
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute search
items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, total)
response := models` + data.NameLower + `.` + data.Name + `GetResponse{
Message: fmt.Sprintf("Search results for '%s'", searchQuery),
Data: items,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}`
}
func generateCreateMethod(data HandlerData) string {
return `
// Create` + data.Name + ` godoc
// @Summary Create ` + data.NameLower + `
// @Description Creates a new ` + data.NameLower + ` record
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param request body models` + data.NameLower + `.` + data.Name + `CreateRequest true "` + data.Name + ` creation request"
// @Success 201 {object} models` + data.NameLower + `.` + data.Name + `CreateResponse "` + data.Name + ` created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NamePlural + ` [post]
func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) {
var req models` + data.NameLower + `.` + data.Name + `CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
// Validate duplicate and daily submission
if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
item, err := h.create` + data.Name + `(ctx, dbConn, &req)
if err != nil {
h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError)
return
}
response := models` + data.NameLower + `.` + data.Name + `CreateResponse{
Message: "` + data.Name + ` berhasil dibuat",
Data: item,
}
c.JSON(http.StatusCreated, response)
}`
}
func generateUpdateMethod(data HandlerData) string {
return `
// Update` + data.Name + ` godoc
// @Summary Update ` + data.NameLower + `
// @Description Updates an existing ` + data.NameLower + ` record
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param id path string true "` + data.Name + ` ID (UUID)"
// @Param request body models` + data.NameLower + `.` + data.Name + `UpdateRequest true "` + data.Name + ` update request"
// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NameLower + `/{id} [put]
func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
var req models` + data.NameLower + `.` + data.Name + `UpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Set ID from path parameter
req.ID = id
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.update` + data.Name + `(ctx, dbConn, &req)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError)
}
return
}
response := models` + data.NameLower + `.` + data.Name + `UpdateResponse{
Message: "` + data.Name + ` berhasil diperbarui",
Data: item,
}
c.JSON(http.StatusOK, response)
}`
}
func generateDeleteMethod(data HandlerData) string {
return `
// Delete` + data.Name + ` godoc
// @Summary Delete ` + data.NameLower + `
// @Description Soft deletes a ` + data.NameLower + ` by setting status to 'deleted'
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param id path string true "` + data.Name + ` ID (UUID)"
// @Success 200 {object} models` + data.NameLower + `.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NameLower + `/{id} [delete]
func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
err = h.delete` + data.Name + `(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError)
}
return
}
response := models` + data.NameLower + `.` + data.Name + `DeleteResponse{
Message: "` + data.Name + ` berhasil dihapus",
ID: id,
}
c.JSON(http.StatusOK, response)
}`
}
func generateStatsMethod(data HandlerData) string {
return `
// Get` + data.Name + `Stats godoc
// @Summary Get ` + data.NameLower + ` statistics
// @Description Returns comprehensive statistics about ` + data.NameLower + ` data
// @Tags ` + data.NameLower + `
// @Accept json
// @Produce json
// @Param status query string false "Filter statistics by status"
// @Success 200 {object} models.AggregateData "Statistics data"
// @Failure 500 {object} models` + data.NameLower + `.ErrorResponse "Internal server error"
// @Router /api/v1/` + data.NamePlural + `/stats [get]
func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) {
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
filter := h.parseFilterParams(c)
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
if err != nil {
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Statistik ` + data.NameLower + ` berhasil diambil",
"data": aggregateData,
})
}`
}
func generateHelperMethods(data HandlerData) string {
helperMethods := `
// Database operations
func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*models` + data.NameLower + `.` + data.Name + `, error) {
query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'"
row := dbConn.QueryRowContext(ctx, query, id)
var item models` + data.NameLower + `.` + data.Name + `
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, err
}
return &item, nil
}
func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) (*models` + data.NameLower + `.` + data.Name + `, error) {
id := uuid.New().String()
now := time.Now()
query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
var item models` + data.NameLower + `.` + data.Name + `
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err)
}
return &item, nil
}
func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `UpdateRequest) (*models` + data.NameLower + `.` + data.Name + `, error) {
now := time.Now()
query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
var item models` + data.NameLower + `.` + data.Name + `
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err)
}
return &item, nil
}
func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error {
now := time.Now()
query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'"
result, err := dbConn.ExecContext(ctx, query, id, now)
if err != nil {
return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter, limit, offset int) ([]models` + data.NameLower + `.` + data.Name + `, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
args = append(args, limit, offset)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err)
}
defer rows.Close()
items := make([]models` + data.NameLower + `.` + data.Name + `, 0, limit)
for rows.Next() {
item, err := h.scan` + data.Name + `(rows)
if err != nil {
return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items))
return items, nil
}`
// Add dynamic fetch method if needed
if data.HasDynamic {
helperMethods += `
// fetchRetribusisDynamic executes dynamic query
func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]models` + data.NameLower + `.` + data.Name + `, int, error) {
// Setup query builder
builder := utils.NewQueryBuilder("` + data.TableName + `").
SetAllowedColumns([]string{
"id", "status", "sort", "user_created", "date_created",
"user_updated", "date_updated", "name",
})
// Add default filter to exclude deleted records
query.Filters = append([]utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpNotEqual,
Value: "deleted",
}},
LogicOp: "AND",
}}, query.Filters...)
// Execute concurrent queries
var (
items []models` + data.NameLower + `.` + data.Name + `
total int
wg sync.WaitGroup
errChan = make(chan error, 2)
mu sync.Mutex
)
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
countQuery := query
countQuery.Limit = 0
countQuery.Offset = 0
countSQL, countArgs, err := builder.BuildCountQuery(countQuery)
if err != nil {
errChan <- fmt.Errorf("failed to build count query: %w", err)
return
}
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
errChan <- fmt.Errorf("failed to get total count: %w", err)
return
}
}()
// Fetch main data
wg.Add(1)
go func() {
defer wg.Done()
mainSQL, mainArgs, err := builder.BuildQuery(query)
if err != nil {
errChan <- fmt.Errorf("failed to build main query: %w", err)
return
}
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
if err != nil {
errChan <- fmt.Errorf("failed to execute main query: %w", err)
return
}
defer rows.Close()
var results []models` + data.NameLower + `.` + data.Name + `
for rows.Next() {
item, err := h.scan` + data.Name + `(rows)
if err != nil {
errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err)
return
}
results = append(results, item)
}
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("rows iteration error: %w", err)
return
}
mu.Lock()
items = results
mu.Unlock()
}()
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return nil, 0, err
}
}
return items, total, nil
}
// Optimized scanning function yang menggunakan sql.Null* types langsung
func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (models` + data.NameLower + `.` + data.Name + `, error) {
var item models` + data.NameLower + `.` + data.Name + `
return item, rows.Scan(
&item.ID, &item.Status, &item.Sort, &item.UserCreated,
&item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name,
)
}`
}
helperMethods += `
func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause)
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
return fmt.Errorf("total count query failed: %w", err)
}
return nil
}
// Get comprehensive aggregate data dengan filter support
func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models` + data.NameLower + `.` + data.Name + `Filter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
}
// Build where clause untuk filter
whereClause, args := h.buildWhereClause(filter)
// Use concurrent execution untuk performance
var wg sync.WaitGroup
var mu sync.Mutex
errChan := make(chan error, 4)
// 1. Count by status
wg.Add(1)
go func() {
defer wg.Done()
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause)
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
if err != nil {
errChan <- fmt.Errorf("status query failed: %w", err)
return
}
defer rows.Close()
mu.Lock()
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
mu.Unlock()
errChan <- fmt.Errorf("status scan failed: %w", err)
return
}
aggregate.ByStatus[status] = count
switch status {
case "active":
aggregate.TotalActive = count
case "draft":
aggregate.TotalDraft = count
case "inactive":
aggregate.TotalInactive = count
}
}
mu.Unlock()
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("status iteration error: %w", err)
}
}()
// 2. Get last updated time dan today statistics
wg.Add(1)
go func() {
defer wg.Done()
// Last updated
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause)
var lastUpdated sql.NullTime
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
errChan <- fmt.Errorf("last updated query failed: %w", err)
return
}
// Today statistics
today := time.Now().Format("2006-01-02")
todayStatsQuery := fmt.Sprintf(` + "`" + `
SELECT
SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today,
SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today
FROM ` + data.TableName + `
WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause)
todayArgs := append(args, today)
var createdToday, updatedToday int
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
errChan <- fmt.Errorf("today stats query failed: %w", err)
return
}
mu.Lock()
if lastUpdated.Valid {
aggregate.LastUpdated = &lastUpdated.Time
}
aggregate.CreatedToday = createdToday
aggregate.UpdatedToday = updatedToday
mu.Unlock()
}()
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return nil, err
}
}
return aggregate, nil
}
// Enhanced error handling
func (h *` + data.Name + `Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
log.Printf("[ERROR] %s: %v", message, err)
h.respondError(c, message, err, statusCode)
}
func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{
Error: errorMessage,
Code: statusCode,
Message: err.Error(),
Timestamp: time.Now(),
})
}
// Parse pagination parameters dengan validation yang lebih ketat
func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) {
limit := 10 // Default limit
offset := 0 // Default offset
if limitStr := c.Query("limit"); limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
}
if parsedLimit <= 0 {
return 0, 0, fmt.Errorf("limit must be greater than 0")
}
if parsedLimit > 100 {
return 0, 0, fmt.Errorf("limit cannot exceed 100")
}
limit = parsedLimit
}
if offsetStr := c.Query("offset"); offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
}
if parsedOffset < 0 {
return 0, 0, fmt.Errorf("offset cannot be negative")
}
offset = parsedOffset
}
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
return limit, offset, nil
}
func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) models` + data.NameLower + `.` + data.Name + `Filter {
filter := models` + data.NameLower + `.` + data.Name + `Filter{}
if status := c.Query("status"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
// Parse date filters
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
filter.DateFrom = &dateFrom
}
}
if dateToStr := c.Query("date_to"); dateToStr != "" {
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
filter.DateTo = &dateTo
}
}
return filter
}
// Build WHERE clause dengan filter parameters
func (h *` + data.Name + `Handler) buildWhereClause(filter models` + data.NameLower + `.` + data.Name + `Filter) (string, []interface{}) {
conditions := []string{"status != 'deleted'"}
args := []interface{}{}
paramCount := 1
if filter.Status != nil {
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
args = append(args, *filter.Status)
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
if filter.DateFrom != nil {
conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
args = append(args, *filter.DateFrom)
paramCount++
}
if filter.DateTo != nil {
conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
paramCount++
}
return strings.Join(conditions, " AND "), args
}
func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages := 0
currentPage := 1
if limit > 0 {
totalPages = (total + limit - 1) / limit // Ceiling division
currentPage = (offset / limit) + 1
}
return models.MetaResponse{
Limit: limit,
Offset: offset,
Total: total,
TotalPages: totalPages,
CurrentPage: currentPage,
HasNext: offset+limit < total,
HasPrev: offset > 0,
}
}
// validate` + data.Name + `Submission performs validation for duplicate entries and daily submission limits
func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) error {
// Import the validation utility
validator := validation.NewDuplicateValidator(dbConn)
// Use default configuration
config := validation.ValidationConfig{
TableName: "` + data.TableName + `",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
}
// Validate duplicate entries with active status for today
err := validator.ValidateDuplicate(ctx, config, "dummy_id")
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Validate once per day submission
err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit")
if err != nil {
return fmt.Errorf("daily submission limit exceeded: %w", err)
}
return nil
}
// Example usage of the validation utility with custom configuration
func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *models` + data.NameLower + `.` + data.Name + `CreateRequest) error {
// Create validator instance
validator := validation.NewDuplicateValidator(dbConn)
// Use custom configuration
config := validation.ValidationConfig{
TableName: "` + data.TableName + `",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
AdditionalFields: map[string]interface{}{
"name": req.Name,
},
}
// Validate with custom fields
fields := map[string]interface{}{
"name": *req.Name,
}
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
if err != nil {
return fmt.Errorf("custom validation failed: %w", err)
}
return nil
}
// GetLastSubmissionTime example
func (h *` + data.Name + `Handler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
validator := validation.NewDuplicateValidator(dbConn)
return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier)
}`
return helperMethods
}
// Keep existing functions for model generation and routes...
// (The remaining functions stay the same as in the original file)
// ================= MODEL GENERATION =====================
func generateModelFile(data HandlerData, modelDir string) {
modelContent := `package models
import (
"` + data.ModuleName + `/internal/models"
"database/sql"
"encoding/json"
"time"
)
// ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table
// with proper null handling and optimized JSON marshaling
type ` + data.Name + ` struct {
ID string ` + "`json:\"id\" db:\"id\"`" + `
Status string ` + "`json:\"status\" db:\"status\"`" + `
Sort models.NullableInt32 ` + "`json:\"sort,omitempty\" db:\"sort\"`" + `
UserCreated sql.NullString ` + "`json:\"user_created,omitempty\" db:\"user_created\"`" + `
DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + `
UserUpdated sql.NullString ` + "`json:\"user_updated,omitempty\" db:\"user_updated\"`" + `
DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + `
Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + `
}
// Custom JSON marshaling untuk ` + data.Name + ` agar NULL values tidak muncul di response
func (r ` + data.Name + `) MarshalJSON() ([]byte, error) {
type Alias ` + data.Name + `
aux := &struct {
Sort *int ` + "`json:\"sort,omitempty\"`" + `
UserCreated *string ` + "`json:\"user_created,omitempty\"`" + `
DateCreated *time.Time ` + "`json:\"date_created,omitempty\"`" + `
UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + `
DateUpdated *time.Time ` + "`json:\"date_updated,omitempty\"`" + `
Name *string ` + "`json:\"name,omitempty\"`" + `
*Alias
}{
Alias: (*Alias)(&r),
}
// Convert NullableInt32 to pointer
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Name.Valid {
aux.Name = &r.Name.String
}
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *` + data.Name + `) GetName() string {
if r.Name.Valid {
return r.Name.String
}
return ""
}`
// Add request/response structs based on enabled methods
if data.HasGet {
modelContent += `
// Response struct untuk GET by ID - diperbaiki struktur
type ` + data.Name + `GetByIDResponse struct {
Message string ` + "`json:\"message\"`" + `
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
}
// Enhanced GET response dengan pagination dan aggregation
type ` + data.Name + `GetResponse struct {
Message string ` + "`json:\"message\"`" + `
Data []` + data.Name + ` ` + "`json:\"data\"`" + `
Meta models.MetaResponse ` + "`json:\"meta\"`" + `
Summary *models.AggregateData ` + "`json:\"summary,omitempty\"`" + `
}`
}
if data.HasPost {
modelContent += `
// Request struct untuk create - dioptimalkan dengan validasi
type ` + data.Name + `CreateRequest struct {
Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + `
Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + `
}
// Response struct untuk create
type ` + data.Name + `CreateResponse struct {
Message string ` + "`json:\"message\"`" + `
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
}`
}
if data.HasPut {
modelContent += `
// Update request - sama seperti create tapi dengan ID
type ` + data.Name + `UpdateRequest struct {
ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + ` // ID dari URL path
Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + `
Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + `
}
// Response struct untuk update
type ` + data.Name + `UpdateResponse struct {
Message string ` + "`json:\"message\"`" + `
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
}`
}
if data.HasDelete {
modelContent += `
// Response struct untuk delete
type ` + data.Name + `DeleteResponse struct {
Message string ` + "`json:\"message\"`" + `
ID string ` + "`json:\"id\"`" + `
}`
}
// Add filter struct
modelContent += `
// Filter struct untuk query parameters
type ` + data.Name + `Filter struct {
Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + `
Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + `
DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + `
DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + `
}`
writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent)
}
// ================= ROUTES GENERATION =====================
func updateRoutesFile(data HandlerData) {
routesFile := "internal/routes/v1/routes.go"
content, err := os.ReadFile(routesFile)
if err != nil {
fmt.Printf("⚠️ Could not read routes.go: %v\n", err)
fmt.Printf("📝 Please manually add these routes to your routes.go file:\n")
printRoutesSample(data)
return
}
routesContent := string(content)
// Build import path
var importPath, importAlias string
if data.Category != "" {
importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName)
importAlias = data.NameLower + "Handlers"
} else {
importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName)
importAlias = data.NameLower + "Handlers"
}
// Add import
importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath)
if !strings.Contains(routesContent, importPattern) {
importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath)
if strings.Contains(routesContent, "import (") {
routesContent = strings.Replace(routesContent, "import (",
"import (\n"+importToAdd, 1)
}
}
// Build new routes in protected group format
newRoutes := generateProtectedRouteBlock(data)
// Insert above protected routes marker
insertMarker := "// ============= PUBLISHED ROUTES ==============================================="
if strings.Contains(routesContent, insertMarker) {
if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) {
// Insert before the marker
routesContent = strings.Replace(routesContent, insertMarker,
newRoutes+"\n\t"+insertMarker, 1)
} else {
fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name)
return
}
} else {
// Fallback: insert at end of setupV1Routes function
setupFuncEnd := "\treturn r"
if strings.Contains(routesContent, setupFuncEnd) {
routesContent = strings.Replace(routesContent, setupFuncEnd,
newRoutes+"\n\n\t"+setupFuncEnd, 1)
}
}
if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil {
fmt.Printf("Error writing routes.go: %v\n", err)
return
}
fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name)
}
func generateProtectedRouteBlock(data HandlerData) string {
routes := fmt.Sprintf(`
// %s endpoints
%sHandler := %sHandlers.New%sHandler()
%sGroup := v1.Group("/%s")
{
%sGroup.GET("", %sHandler.Get%s)`,
strings.Title(data.NamePlural), data.NameLower, data.NameLower, data.Name,
data.NameLower, data.NameLower,
data.NameLower, data.NameLower, data.Name)
if data.HasDynamic {
routes += fmt.Sprintf(`
%sGroup.GET("/dynamic", %sHandler.Get%sDynamic) // Route baru`,
data.NameLower, data.NameLower, data.Name)
}
if data.HasSearch {
routes += fmt.Sprintf(`
%sGroup.GET("/search", %sHandler.Search%sAdvanced) // Route pencarian`,
data.NameLower, data.NameLower, data.Name)
}
routes += fmt.Sprintf(`
%sGroup.GET("/:id", %sHandler.Get%sByID)`,
data.NameLower, data.NameLower, data.Name)
if data.HasPost {
routes += fmt.Sprintf(`
%sGroup.POST("", %sHandler.Create%s)`,
data.NameLower, data.NameLower, data.Name)
}
if data.HasPut {
routes += fmt.Sprintf(`
%sGroup.PUT("/:id", %sHandler.Update%s)`,
data.NameLower, data.NameLower, data.Name)
}
if data.HasDelete {
routes += fmt.Sprintf(`
%sGroup.DELETE("/:id", %sHandler.Delete%s)`,
data.NameLower, data.NameLower, data.Name)
}
if data.HasStats {
routes += fmt.Sprintf(`
%sGroup.GET("/stats", %sHandler.Get%sStats)`,
data.NameLower, data.NameLower, data.Name)
}
routes += `
}`
return routes
}
func printRoutesSample(data HandlerData) {
fmt.Print(generateProtectedRouteBlock(data))
fmt.Println()
}
// ================= UTILITY FUNCTIONS =====================
func writeFile(filename, content string) {
if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
fmt.Printf("❌ Error creating file %s: %v\n", filename, err)
return
}
fmt.Printf("✅ Generated: %s\n", filename)
}