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 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") fmt.Println(" go run generate-handler.go reference/peserta get post put delete") fmt.Println(" go run generate-handler.go master/wilayah get") 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 if none specified methods = []string{"get", "post", "put", "delete"} } // 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 } } // Always add stats if we have get if data.HasGet { data.HasStats = true } // Create directories with improved logic var handlerDir, modelDir string if category != "" { // Dengan kategori: internal/handlers/category/ handlerDir = filepath.Join("internal", "handlers", category) modelDir = filepath.Join("internal", "models", category) } else { // Tanpa kategori: langsung internal/handlers/ handlerDir = filepath.Join("internal", "handlers") modelDir = filepath.Join("internal", "models") } // Buat direktori 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 "` + modelsImportPath + `" "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.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 { // Build route path based on category var routePath, singleRoutePath string if data.Category != "" { routePath = data.Category + "/" + data.NamePlural singleRoutePath = data.Category + "/" + data.NameLower } else { routePath = data.NamePlural singleRoutePath = data.NameLower } 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.Name + `GetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + routePath + ` [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("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.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.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.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/` + singleRoutePath + `/{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("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.Name + `GetByIDResponse{ Message: "` + data.Name + ` details retrieved successfully", Data: item, } c.JSON(http.StatusOK, response) }` } func generateCreateMethod(data HandlerData) string { var routePath string if data.Category != "" { routePath = data.Category + "/" + data.NamePlural } else { routePath = data.NamePlural } 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.Name + `CreateRequest true "` + data.Name + ` creation request" // @Success 201 {object} models.` + 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/` + routePath + ` [post] func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { var req models.` + 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("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.create` + data.Name + `(ctx, dbConn, &req) if err != nil { h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError) return } response := models.` + data.Name + `CreateResponse{ Message: "` + data.Name + ` berhasil dibuat", Data: item, } c.JSON(http.StatusCreated, response) }` } func generateUpdateMethod(data HandlerData) string { var singleRoutePath string if data.Category != "" { singleRoutePath = data.Category + "/" + data.NameLower } else { singleRoutePath = data.NameLower } 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.Name + `UpdateRequest true "` + data.Name + ` update request" // @Success 200 {object} models.` + 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/` + singleRoutePath + `/{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.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("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.Name + `UpdateResponse{ Message: "` + data.Name + ` berhasil diperbarui", Data: item, } c.JSON(http.StatusOK, response) }` } func generateDeleteMethod(data HandlerData) string { var singleRoutePath string if data.Category != "" { singleRoutePath = data.Category + "/" + data.NameLower } else { singleRoutePath = data.NameLower } 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.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/` + singleRoutePath + `/{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("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.Name + `DeleteResponse{ Message: "` + data.Name + ` berhasil dihapus", ID: id, } c.JSON(http.StatusOK, response) }` } func generateStatsMethod(data HandlerData) string { var routePath string if data.Category != "" { routePath = data.Category + "/" + data.NamePlural } else { routePath = data.NamePlural } 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.ErrorResponse "Internal server error" // @Router /api/v1/` + routePath + `/stats [get] func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { dbConn, err := h.db.GetDB("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 { return ` // Database operations func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*models.` + data.Name + `, error) { query := "SELECT id, status, date_created, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" row := dbConn.QueryRowContext(ctx, query, id) var item models.` + data.Name + ` err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &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.Name + `CreateRequest) (*models.` + 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, date_created, date_updated, name" row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) var item models.` + data.Name + ` err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &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.Name + `UpdateRequest) (*models.` + 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, date_created, date_updated, name" row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) var item models.` + data.Name + ` err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &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.Name + `Filter, limit, offset int) ([]models.` + data.Name + `, error) { whereClause, args := h.buildWhereClause(filter) query := fmt.Sprintf("SELECT id, status, date_created, 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.Name + `, 0, limit) for rows.Next() { var item models.` + data.Name + ` err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) 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) } return items, nil } func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.` + 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 } func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.` + data.Name + `Filter) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } whereClause, args := h.buildWhereClause(filter) 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 { return nil, fmt.Errorf("status query failed: %w", err) } defer rows.Close() for rows.Next() { var status string var count int if err := rows.Scan(&status, &count); err != nil { return nil, fmt.Errorf("status scan failed: %w", err) } aggregate.ByStatus[status] = count switch status { case "active": aggregate.TotalActive = count case "draft": aggregate.TotalDraft = count case "inactive": aggregate.TotalInactive = count } } return aggregate, nil } // Helper methods 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(), }) } 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 } return limit, offset, nil } func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) models.` + data.Name + `Filter { filter := models.` + 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 } func (h *` + data.Name + `Handler) buildWhereClause(filter models.` + 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, } }` } // ================= MODEL GENERATION ===================== func generateModelFile(data HandlerData, modelDir string) { modelContent := `package models import ( "database/sql" "database/sql/driver" "encoding/json" "time" ) // NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility type NullableInt32 struct { Int32 int32 ` + "`json:\"int32,omitempty\"`" + ` Valid bool ` + "`json:\"valid\"`" + ` } // Scan implements the sql.Scanner interface for NullableInt32 func (n *NullableInt32) Scan(value interface{}) error { var ni sql.NullInt32 if err := ni.Scan(value); err != nil { return err } n.Int32 = ni.Int32 n.Valid = ni.Valid return nil } // Value implements the driver.Valuer interface for NullableInt32 func (n NullableInt32) Value() (driver.Value, error) { if !n.Valid { return nil, nil } return n.Int32, nil } // ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table type ` + data.Name + ` struct { ID string ` + "`json:\"id\" db:\"id\"`" + ` Status string ` + "`json:\"status\" db:\"status\"`" + ` Sort 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 for ` + data.Name + ` 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), } 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 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 for GET by ID type ` + data.Name + `GetByIDResponse struct { Message string ` + "`json:\"message\"`" + ` Data *` + data.Name + ` ` + "`json:\"data\"`" + ` } // Enhanced GET response with pagination and aggregation type ` + data.Name + `GetResponse struct { Message string ` + "`json:\"message\"`" + ` Data []` + data.Name + ` ` + "`json:\"data\"`" + ` Meta MetaResponse ` + "`json:\"meta\"`" + ` Summary *AggregateData ` + "`json:\"summary,omitempty\"`" + ` }` } if data.HasPost { modelContent += ` // Request struct for create 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 for create type ` + data.Name + `CreateResponse struct { Message string ` + "`json:\"message\"`" + ` Data *` + data.Name + ` ` + "`json:\"data\"`" + ` }` } if data.HasPut { modelContent += ` // Update request 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 for update type ` + data.Name + `UpdateResponse struct { Message string ` + "`json:\"message\"`" + ` Data *` + data.Name + ` ` + "`json:\"data\"`" + ` }` } if data.HasDelete { modelContent += ` // Response struct for delete type ` + data.Name + `DeleteResponse struct { Message string ` + "`json:\"message\"`" + ` ID string ` + "`json:\"id\"`" + ` }` } // Add common structs modelContent += ` // Metadata for pagination type MetaResponse struct { Limit int ` + "`json:\"limit\"`" + ` Offset int ` + "`json:\"offset\"`" + ` Total int ` + "`json:\"total\"`" + ` TotalPages int ` + "`json:\"total_pages\"`" + ` CurrentPage int ` + "`json:\"current_page\"`" + ` HasNext bool ` + "`json:\"has_next\"`" + ` HasPrev bool ` + "`json:\"has_prev\"`" + ` } // Aggregate data for summary type AggregateData struct { TotalActive int ` + "`json:\"total_active\"`" + ` TotalDraft int ` + "`json:\"total_draft\"`" + ` TotalInactive int ` + "`json:\"total_inactive\"`" + ` ByStatus map[string]int ` + "`json:\"by_status\"`" + ` LastUpdated *time.Time ` + "`json:\"last_updated,omitempty\"`" + ` CreatedToday int ` + "`json:\"created_today\"`" + ` UpdatedToday int ` + "`json:\"updated_today\"`" + ` } // Error response type ErrorResponse struct { Error string ` + "`json:\"error\"`" + ` Code int ` + "`json:\"code\"`" + ` Message string ` + "`json:\"message\"`" + ` Timestamp time.Time ` + "`json:\"timestamp\"`" + ` } // Filter struct for 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\"`" + ` } // Validation constants const ( StatusDraft = "draft" StatusActive = "active" StatusInactive = "inactive" StatusDeleted = "deleted" ) // ValidStatuses for validation var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive} // IsValidStatus helper function func IsValidStatus(status string) bool { for _, validStatus := range ValidStatuses { if status == validStatus { return true } } return false }` 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 - PERBAIKAN UTAMA var importPath, importAlias string if data.Category != "" { importPath = fmt.Sprintf("%s/internal/handlers/%s", data.ModuleName, data.Category) importAlias = data.NameLower + "Handlers" } else { importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) importAlias = data.NameLower + "Handlers" } // 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 route paths var routesPath, singleRoutePath string if data.Category != "" { routesPath = data.Category + "/" + data.NamePlural singleRoutePath = data.Category + "/" + data.NameLower } else { routesPath = data.NamePlural singleRoutePath = data.NameLower } // routes newRoutes := fmt.Sprintf("\t\t// %s endpoints\n", data.Name) newRoutes += fmt.Sprintf("\t\t%sHandler := %s.New%sHandler()\n", data.NameLower, importAlias, data.Name) if data.HasGet { newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s\", %sHandler.Get%s)\n", routesPath, data.NameLower, data.Name) newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasPost { newRoutes += fmt.Sprintf("\t\tv1.POST(\"/%s\", %sHandler.Create%s)\n", routesPath, data.NameLower, data.Name) } if data.HasPut { newRoutes += fmt.Sprintf("\t\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasDelete { newRoutes += fmt.Sprintf("\t\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasStats { newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s/stats\", %sHandler.Get%sStats)\n", routesPath, data.NameLower, data.Name) } newRoutes += "\n" insertMarker := "\t\tprotected := v1.Group(\"/\")" if strings.Contains(routesContent, insertMarker) { if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { routesContent = strings.Replace(routesContent, insertMarker, newRoutes+insertMarker, 1) } else { fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name) return } } 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 printRoutesSample(data HandlerData) { var routesPath, singleRoutePath string if data.Category != "" { routesPath = data.Category + "/" + data.NamePlural singleRoutePath = data.Category + "/" + data.NameLower } else { routesPath = data.NamePlural singleRoutePath = data.NameLower } var importAlias string if data.Category != "" { importAlias = data.NameLower + "Handlers" } else { importAlias = data.NameLower + "Handlers" } fmt.Printf(` // %s endpoints %sHandler := %s.New%sHandler() `, data.Name, data.NameLower, importAlias, data.Name) if data.HasGet { fmt.Printf("\tv1.GET(\"/%s\", %sHandler.Get%s)\n", routesPath, data.NameLower, data.Name) fmt.Printf("\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasPost { fmt.Printf("\tv1.POST(\"/%s\", %sHandler.Create%s)\n", routesPath, data.NameLower, data.Name) } if data.HasPut { fmt.Printf("\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasDelete { fmt.Printf("\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", singleRoutePath, data.NameLower, data.Name) } if data.HasStats { fmt.Printf("\tv1.GET(\"/%s/stats\", %sHandler.Get%sStats)\n", routesPath, data.NameLower, data.Name) } 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) }