package main import ( "flag" "fmt" "log" "os" "path/filepath" "strings" "time" "gopkg.in/yaml.v2" ) // HandlerData contains template data for handler generation type HandlerData struct { Name string NameLower string NamePlural string Category string // Untuk backward compatibility (bagian pertama) DirPath string // Path direktori lengkap ModuleName string TableName string HasGet bool HasPost bool HasPut bool HasDelete bool HasStats bool HasDynamic bool HasSearch bool HasFilter bool HasPagination bool Timestamp string } // Config represents the YAML configuration structure type Config struct { Entities []Entity `yaml:"entities"` } // Entity represents a single entity configuration in YAML type Entity struct { Name string `yaml:"name"` Methods []string `yaml:"methods"` Category string `yaml:"category,omitempty"` } // ServicesConfig represents the new services-based YAML configuration structure type ServicesConfig struct { Global GlobalConfig `yaml:"global"` Services map[string]ServiceConfig `yaml:"services"` } // GlobalConfig represents global configuration type GlobalConfig struct { ModuleName string `yaml:"module_name"` OutputDir string `yaml:"output_dir"` EnableSwagger bool `yaml:"enable_swagger"` EnableLogging bool `yaml:"enable_logging"` } // ServiceConfig represents a service configuration type ServiceConfig struct { Name string `yaml:"name"` Category string `yaml:"category"` Package string `yaml:"package"` Description string `yaml:"description"` BaseURL string `yaml:"base_url"` Timeout int `yaml:"timeout"` RetryCount int `yaml:"retry_count"` Endpoints map[string]EndpointConfig `yaml:"endpoints"` } // EndpointConfig represents an endpoint configuration type EndpointConfig struct { Description string `yaml:"description"` HandlerFolder string `yaml:"handler_folder"` HandlerFile string `yaml:"handler_file"` HandlerName string `yaml:"handler_name"` TableName string `yaml:"table_name,omitempty"` Functions map[string]FunctionConfig `yaml:"functions"` } // FunctionConfig represents a function configuration type FunctionConfig struct { Methods []string `yaml:"methods"` Path string `yaml:"path"` GetRoutes string `yaml:"get_routes,omitempty"` PostRoutes string `yaml:"post_routes,omitempty"` PutRoutes string `yaml:"put_routes,omitempty"` DeleteRoutes string `yaml:"delete_routes,omitempty"` GetPath string `yaml:"get_path,omitempty"` PostPath string `yaml:"post_path,omitempty"` PutPath string `yaml:"put_path,omitempty"` DeletePath string `yaml:"delete_path,omitempty"` Model string `yaml:"model"` ResponseModel string `yaml:"response_model"` RequestModel string `yaml:"request_model,omitempty"` Description string `yaml:"description"` Summary string `yaml:"summary"` Tags []string `yaml:"tags"` RequireAuth bool `yaml:"require_auth"` CacheEnabled bool `yaml:"cache_enabled"` EnableDatabase bool `yaml:"enable_database"` CacheTTL int `yaml:"cache_ttl"` HasPagination bool `yaml:"has_pagination,omitempty"` HasFilter bool `yaml:"has_filter,omitempty"` HasSearch bool `yaml:"has_search,omitempty"` HasStats bool `yaml:"has_stats,omitempty"` HasDynamic bool `yaml:"has_dynamic,omitempty"` } type PathInfo struct { Category string EntityName string DirPath string FilePath string } // Global variables for command line flags var ( forceFlag = flag.Bool("force", false, "Force overwrite existing files") verboseFlag = flag.Bool("verbose", false, "Enable verbose output") configFlag = flag.String("config", "", "Specify config file path") ) // Global file skip function var shouldSkipExistingFile func(filePath string, fileType string) bool // parseEntityPath - Enhanced logic parsing dengan validasi lebih baik func parseEntityPath(entityPath string) (*PathInfo, error) { if strings.TrimSpace(entityPath) == "" { return nil, fmt.Errorf("entity path cannot be empty") } var pathInfo PathInfo // Clean path untuk menghapus leading/trailing slashes cleanedPath := strings.Trim(entityPath, "/") parts := strings.Split(cleanedPath, "/") // Validasi minimal 1 bagian (file saja) dan maksimal 4 if len(parts) < 1 || len(parts) > 4 { return nil, fmt.Errorf("invalid path format: use up to 4 levels like 'level1/level2/level3/entity'") } // Validasi bagian kosong dan karakter tidak valid for i, part := range parts { if strings.TrimSpace(part) == "" { return nil, fmt.Errorf("empty path segment at position %d", i+1) } // Validasi karakter untuk keamanan if !isValidPathSegment(part) { return nil, fmt.Errorf("invalid characters in path segment '%s' at position %d", part, i+1) } } pathInfo.EntityName = parts[len(parts)-1] if len(parts) > 1 { pathInfo.Category = parts[len(parts)-2] pathInfo.DirPath = strings.Join(parts[:len(parts)-1], "/") pathInfo.FilePath = pathInfo.DirPath + "/" + strings.ToLower(pathInfo.EntityName) + ".go" } else { pathInfo.Category = "models" pathInfo.DirPath = "" pathInfo.FilePath = strings.ToLower(pathInfo.EntityName) + ".go" } return &pathInfo, nil } // Validasi karakter untuk path segment func isValidPathSegment(segment string) bool { // Hanya izinkan alphanumeric, underscore, dan dash for _, char := range segment { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-') { return false } } return true } // validateMethods - Validasi method yang diinput func validateMethods(methods []string) error { validMethods := map[string]bool{ "get": true, "post": true, "put": true, "delete": true, "stats": true, "dynamic": true, "search": true, } for _, method := range methods { if !validMethods[strings.ToLower(method)] { return fmt.Errorf("invalid method: %s. Valid methods: get, post, put, delete, stats, dynamic, search", method) } } return nil } // generateTableName - Generate table name berdasarkan path lengkap func generateTableName(pathInfo *PathInfo) string { entityLower := strings.ToLower(pathInfo.EntityName) if pathInfo.DirPath != "" { // Replace "/" dengan "_" untuk table name pathForTable := strings.ReplaceAll(pathInfo.DirPath, "/", "_") return "data_" + pathForTable + "_" + entityLower } return "data_" + entityLower } // createDirectories - Enhanced directory creation dengan better error handling func createDirectories(pathInfo *PathInfo) (string, string, error) { var handlerDir, modelDir string // Support nested directories lebih baik if pathInfo.DirPath != "" { // Normalize path untuk memastikan konsistensi normalizedPath := filepath.Clean(pathInfo.DirPath) handlerDir = filepath.Join("internal", "handlers", normalizedPath) modelDir = filepath.Join("internal", "models", normalizedPath) } else { handlerDir = filepath.Join("internal", "handlers") modelDir = filepath.Join("internal", "models") } // Create directories dengan permission yang tepat for _, dir := range []string{handlerDir, modelDir} { if err := os.MkdirAll(dir, 0755); err != nil { return "", "", fmt.Errorf("failed to create directory %s: %v", dir, err) } if *verboseFlag { fmt.Printf("๐Ÿ“ Created directory: %s\n", dir) } } return handlerDir, modelDir, nil } // setMethods - Set method flags berdasarkan input func setMethods(data *HandlerData, methods []string) { methodMap := map[string]*bool{ "get": &data.HasGet, "post": &data.HasPost, "put": &data.HasPut, "delete": &data.HasDelete, "stats": &data.HasStats, "dynamic": &data.HasDynamic, "search": &data.HasSearch, } for _, method := range methods { if flag, exists := methodMap[strings.ToLower(method)]; exists { *flag = true } } // Always add stats if we have get if data.HasGet { data.HasStats = true } } // Helper function untuk remove duplicate methods func removeDuplicateMethods(methods []string) []string { seen := make(map[string]bool) result := []string{} for _, method := range methods { lowerMethod := strings.ToLower(method) if !seen[lowerMethod] { seen[lowerMethod] = true result = append(result, method) } } return result } // Validasi apakah file sudah ada dan harus di-skip func defaultShouldSkipExistingFile(filePath string, fileType string) bool { if *forceFlag { if *verboseFlag { fmt.Printf("๐Ÿ”„ Force mode enabled, overwriting: %s\n", filePath) } return false } if _, err := os.Stat(filePath); err == nil { fmt.Printf("โš ๏ธ %s file already exists: %s\n", strings.Title(fileType), filePath) if *verboseFlag { fmt.Printf(" Use --force to overwrite\n") } return true } return false } func loadConfig(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var config Config if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse YAML config: %w", err) } return &config, nil } func loadServicesConfig(configPath string) (*ServicesConfig, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read services config file: %w", err) } var config ServicesConfig if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse YAML services config: %w", err) } return &config, nil } // generateFromServicesConfig - RESTRUCTURED untuk agreggasi methods func generateFromServicesConfig(config *ServicesConfig) { for serviceName, service := range config.Services { if *verboseFlag { fmt.Printf("๐Ÿ”ง Processing service: %s\n", serviceName) } for endpointName, endpoint := range service.Endpoints { if *verboseFlag { fmt.Printf(" ๐Ÿ“ Processing endpoint: %s\n", endpointName) } // Parse entity path dari endpoint name pathInfo, err := parseEntityPath(endpointName) if err != nil { logError(fmt.Sprintf("Error parsing entity path '%s'", endpointName), err, *verboseFlag) continue } // Override category dari service config if service.Category != "" { pathInfo.Category = service.Category } // Set directory path dari handler_folder jika specified if endpoint.HandlerFolder != "" { pathInfo.DirPath = endpoint.HandlerFolder } // AGGREGATE semua methods dari semua functions var allMethods []string var functionConfigs []FunctionConfig for functionName, function := range endpoint.Functions { if *verboseFlag { fmt.Printf(" โš™๏ธ Processing function: %s\n", functionName) } // Tambahkan methods dari function ini allMethods = append(allMethods, function.Methods...) functionConfigs = append(functionConfigs, function) } // Remove duplicates dari methods allMethods = removeDuplicateMethods(allMethods) // Jika tidak ada methods, gunakan default if len(allMethods) == 0 { allMethods = []string{"get", "post", "put", "delete", "dynamic", "search"} } // Validate methods if err := validateMethods(allMethods); err != nil { logError(fmt.Sprintf("Invalid methods for endpoint '%s'", endpointName), err, *verboseFlag) continue } // Override table name jika specified tableName := endpoint.TableName if tableName == "" { tableName = generateTableName(pathInfo) } // Generate handler data dengan service-specific information entityName := strings.Title(pathInfo.EntityName) entityLower := strings.ToLower(pathInfo.EntityName) entityPlural := entityLower + "s" data := HandlerData{ Name: entityName, NameLower: entityLower, NamePlural: entityPlural, Category: pathInfo.Category, DirPath: pathInfo.DirPath, ModuleName: config.Global.ModuleName, TableName: tableName, Timestamp: time.Now().Format("2006-01-02 15:04:05"), } // Set methods berdasarkan aggregated methods setMethods(&data, allMethods) // Set flags berdasarkan function configs for _, function := range functionConfigs { if function.HasPagination { data.HasPagination = true } if function.HasFilter { data.HasFilter = true } if function.HasSearch { data.HasSearch = true } if function.HasStats { data.HasStats = true } if function.HasDynamic { data.HasDynamic = true } } // Create directories handlerDir, modelDir, err := createDirectories(pathInfo) if err != nil { logError("Error creating directories", err, *verboseFlag) continue } // CHECK existing files sebelum generate handlerPath := filepath.Join(handlerDir, entityLower+".go") modelPath := filepath.Join(modelDir, entityLower+".go") if shouldSkipExistingFile(handlerPath, "handler") { fmt.Printf("โš ๏ธ Skipping handler generation: %s\n", handlerPath) continue } if shouldSkipExistingFile(modelPath, "model") { fmt.Printf("โš ๏ธ Skipping model generation: %s\n", modelPath) continue } // Generate files (SEKALI SAJA per endpoint) generateHandlerFile(data, handlerDir) generateModelFile(data, modelDir) // HANYA UPDATE ROUTES SEKALI PER ENDPOINT setelah semua fungsi di-aggregate updateRoutesFile(data) // Success output logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), fmt.Sprintf("Category: %s", pathInfo.Category), fmt.Sprintf("Path: %s", pathInfo.DirPath), fmt.Sprintf("Handler: %s", handlerPath), fmt.Sprintf("Model: %s", modelPath), fmt.Sprintf("Table: %s", tableName), fmt.Sprintf("Methods: %s", strings.Join(allMethods, ", ")), ) } } } func main() { // Parse command line flags flag.Parse() // Set global file skip function shouldSkipExistingFile = defaultShouldSkipExistingFile // Determine config file path configPath := "services-config.yaml" if *configFlag != "" { configPath = *configFlag } // Check for services-config.yaml first (new format) servicesConfig, err := loadServicesConfig(configPath) if err == nil { // Use services config if *verboseFlag { fmt.Printf("๐Ÿ“„ Using services configuration from %s\n", configPath) } generateFromServicesConfig(servicesConfig) return } // Fallback to old config-handler.yml oldConfigPath := "config-handler.yml" if *configFlag == "" { oldConfigPath = "config-handler.yml" } config, err := loadConfig(oldConfigPath) if err == nil { // Generate from old config if *verboseFlag { fmt.Printf("๐Ÿ“„ Using legacy configuration from %s\n", oldConfigPath) } for _, entity := range config.Entities { pathInfo, err := parseEntityPath(entity.Name) if err != nil { logError(fmt.Sprintf("Error parsing entity path '%s'", entity.Name), err, *verboseFlag) continue } // Override category if specified in config if entity.Category != "" { pathInfo.Category = entity.Category } // Use methods from config or default methods := entity.Methods if len(methods) == 0 { methods = []string{"get", "post", "put", "delete", "dynamic", "search"} } // Validate methods if err := validateMethods(methods); err != nil { logError(fmt.Sprintf("Invalid methods for entity '%s'", entity.Name), err, *verboseFlag) continue } generateForEntity(pathInfo, methods) } return } // No config files found, fallback to command line arguments fmt.Printf("โš ๏ธ No config files found (%s or %s), falling back to command line arguments\n", configPath, oldConfigPath) if len(os.Args) < 2 { fmt.Println("Usage: go run generate-handler.go [path/]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") fmt.Println(" go run generate-handler.go product/category/subcategory/item get post") fmt.Println("\nSupported methods: get, post, put, delete, stats, dynamic, search") fmt.Println("\nAlternatively, create a services-config.yaml or config-handler.yml file with configurations.") fmt.Println("\nFlags:") fmt.Println(" --force Force overwrite existing files") fmt.Println(" --verbose Enable verbose output") fmt.Println(" --config Specify config file path") os.Exit(1) } // Parse entity path entityPath := strings.TrimSpace(os.Args[1]) pathInfo, err := parseEntityPath(entityPath) if err != nil { logError("Error parsing path", err, true) os.Exit(1) } // Parse methods var 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"} } // Validate methods if err := validateMethods(methods); err != nil { logError("Method validation failed", err, true) os.Exit(1) } // Generate for single entity generateForEntity(pathInfo, methods) } func generateForEntity(pathInfo *PathInfo, methods []string) { // Format names entityName := strings.Title(pathInfo.EntityName) // PascalCase entity name entityLower := strings.ToLower(pathInfo.EntityName) entityPlural := entityLower + "s" // Generate table name tableName := generateTableName(pathInfo) // Create HandlerData data := HandlerData{ Name: entityName, NameLower: entityLower, NamePlural: entityPlural, Category: pathInfo.Category, DirPath: pathInfo.DirPath, ModuleName: "api-service", TableName: tableName, HasPagination: true, HasFilter: true, Timestamp: time.Now().Format("2006-01-02 15:04:05"), } // Set methods setMethods(&data, methods) // Create directories handlerDir, modelDir, err := createDirectories(pathInfo) if err != nil { logError("Error creating directories", err, *verboseFlag) return } // CHECK existing files sebelum generate handlerPath := filepath.Join(handlerDir, entityLower+".go") modelPath := filepath.Join(modelDir, entityLower+".go") if shouldSkipExistingFile(handlerPath, "handler") { fmt.Printf("โš ๏ธ Skipping handler generation: %s\n", handlerPath) return } if shouldSkipExistingFile(modelPath, "model") { fmt.Printf("โš ๏ธ Skipping model generation: %s\n", modelPath) return } // Generate files generateHandlerFile(data, handlerDir) generateModelFile(data, modelDir) updateRoutesFile(data) // Ini untuk legacy mode, masih ok karena hanya sekali per entity // Success output logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), fmt.Sprintf("Category: %s", pathInfo.Category), fmt.Sprintf("Path: %s", pathInfo.DirPath), fmt.Sprintf("Handler: %s", handlerPath), fmt.Sprintf("Model: %s", modelPath), fmt.Sprintf("Table: %s", tableName), fmt.Sprintf("Methods: %s", strings.Join(methods, ", ")), ) } // ================= HANDLER GENERATION ===================== func generateHandlerFile(data HandlerData, handlerDir string) { var handlerContent strings.Builder // Header handlerContent.WriteString("package handlers\n\n") handlerContent.WriteString("import (\n") handlerContent.WriteString(` "` + data.ModuleName + `/internal/config"` + "\n") handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n") handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n") if data.Category != "models" { handlerContent.WriteString(data.Category + `Models "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") } // Conditional imports based on enabled methods if data.HasDynamic || data.HasSearch { handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n") } // Only import validation if POST is enabled (since validation is primarily for create operations) if data.HasPost { handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\n") } handlerContent.WriteString(` "context"` + "\n") handlerContent.WriteString(` "database/sql"` + "\n") handlerContent.WriteString(` "fmt"` + "\n") handlerContent.WriteString(` "log"` + "\n") handlerContent.WriteString(` "net/http"` + "\n") handlerContent.WriteString(` "strconv"` + "\n") handlerContent.WriteString(` "strings"` + "\n") handlerContent.WriteString(` "sync"` + "\n") handlerContent.WriteString(` "time"` + "\n\n") handlerContent.WriteString(` "github.com/gin-gonic/gin"` + "\n") handlerContent.WriteString(` "github.com/go-playground/validator/v10"` + "\n") handlerContent.WriteString(` "github.com/google/uuid"` + "\n") handlerContent.WriteString(")\n\n") // Vars handlerContent.WriteString("var (\n") handlerContent.WriteString(" " + data.NameLower + "db database.Service\n") handlerContent.WriteString(" " + data.NameLower + "once sync.Once\n") handlerContent.WriteString(" " + data.NameLower + "validate *validator.Validate\n") handlerContent.WriteString(")\n\n") // init func handlerContent.WriteString("// Initialize the database connection and validator\n") handlerContent.WriteString("func init() {\n") handlerContent.WriteString(" " + data.NameLower + "once.Do(func() {\n") handlerContent.WriteString(" " + data.NameLower + "db = database.New(config.LoadConfig())\n") handlerContent.WriteString(" " + data.NameLower + "validate = validator.New()\n") // Only register validation if POST is enabled if data.HasPost { handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") } handlerContent.WriteString(" if " + data.NameLower + "db == nil {\n") handlerContent.WriteString(" log.Fatal(\"Failed to initialize database connection\")\n") handlerContent.WriteString(" }\n") handlerContent.WriteString(" })\n") handlerContent.WriteString("}\n\n") // Custom validation - Only include if POST is enabled if data.HasPost { handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n") handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n") handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n") handlerContent.WriteString("}\n\n") } // Handler struct handlerContent.WriteString("// " + data.Name + "Handler handles " + data.NameLower + " services\n") handlerContent.WriteString("type " + data.Name + "Handler struct {\n") handlerContent.WriteString(" db database.Service\n") handlerContent.WriteString("}\n\n") // Constructor handlerContent.WriteString("// New" + data.Name + "Handler creates a new " + data.Name + "Handler\n") handlerContent.WriteString("func New" + data.Name + "Handler() *" + data.Name + "Handler {\n") handlerContent.WriteString(" return &" + data.Name + "Handler{\n") handlerContent.WriteString(" db: " + data.NameLower + "db,\n") handlerContent.WriteString(" }\n") handlerContent.WriteString("}\n") // Add optional methods based on enabled flags if data.HasGet { handlerContent.WriteString(generateGetMethods(data)) } if data.HasDynamic { handlerContent.WriteString(generateDynamicMethod(data)) } if data.HasSearch { handlerContent.WriteString(generateSearchMethod(data)) } if data.HasPost { handlerContent.WriteString(generateCreateMethod(data)) } if data.HasPut { handlerContent.WriteString(generateUpdateMethod(data)) } if data.HasDelete { handlerContent.WriteString(generateDeleteMethod(data)) } if data.HasStats { handlerContent.WriteString(generateStatsMethod(data)) } // Add helper methods - this function now handles conditional generation internally handlerContent.WriteString(generateHelperMethods(data)) // Write into file writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent.String()) } 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.Name + ` // @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} ` + data.Category + `Models.` + 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 []` + data.Category + `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 := ` + data.Category + `Models.` + data.Name + `GetResponse{ Message: "Data ` + data.Category + ` 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.Name + ` // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" // @Success 200 {object} ` + data.Category + `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/` + 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 := ` + data.Category + `Models.` + data.Name + `GetByIDResponse{ Message: "` + data.Category + ` 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.Name + ` // @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} ` + data.Category + `Models.` + 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 := ` + data.Category + `Models.` + data.Name + `GetResponse{ Message: "Data ` + data.Category + ` berhasil diambil", Data: items, Meta: meta, } c.JSON(http.StatusOK, response) }` } func generateSearchMethod(data HandlerData) string { return ` // Get` + data.Name + `Search godoc // @Summary Get ` + data.NameLower + ` with Search filtering // @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus // @Tags ` + data.Name + ` // @Accept json // @Produce json // @Param fields query string false "Fields to select (e.g., fields=*.*)" // @Param filter[column][operator] query string false "Search 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} ` + data.Category + `Models.` + 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 + `/search [get] func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) { // Parse complex search parameters searchQuery := c.Query("q") if searchQuery == "" { // If no search query provided, return all records with default sorting query := utils.DynamicQuery{ Fields: []string{"*"}, Filters: []utils.FilterGroup{}, // Empty filters - fetch` + data.Name + `sDynamic will add default deleted filter 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 query to get all records ` + data.NameLower + `s, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) if err != nil { h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) return } // Build response meta := h.calculateMeta(query.Limit, query.Offset, total) response := ` + data.Category + `Models.` + data.Name + `GetResponse{ Message: "All records retrieved (no search query provided)", Data: ` + data.NameLower + `s, Meta: meta, } c.JSON(http.StatusOK, response) return } // Build dynamic query for search query := utils.DynamicQuery{ Fields: []string{"*"}, Filters: []utils.FilterGroup{{ Filters: []utils.DynamicFilter{ { 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 ` + data.NameLower + `s, 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 := ` + data.Category + `Models.` + data.Name + `GetResponse{ Message: fmt.Sprintf("Search results for '%s'", searchQuery), Data: ` + data.NameLower + `s, Meta: meta, } c.JSON(http.StatusOK, response) } // fetch` + data.Name + `sDynamic executes dynamic query func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) { // Setup query builders countBuilder := utils.NewQueryBuilder("` + data.TableName + `"). SetColumnMapping(map[string]string{ // Add your column mappings here }). SetAllowedColumns([]string{ "id", "status", "sort", "user_created", "date_created", "user_updated", "date_updated", "name", // Add other allowed columns here }) mainBuilder := utils.NewQueryBuilder("` + data.TableName + `"). SetColumnMapping(map[string]string{ // Add your column mappings here }). SetAllowedColumns([]string{ "id", "status", "sort", "user_created", "date_created", "user_updated", "date_updated", "name", // Add other allowed columns here }) // Add default filter to exclude deleted records if len(query.Filters) > 0 { query.Filters = append([]utils.FilterGroup{{ Filters: []utils.DynamicFilter{{ Column: "status", Operator: utils.OpNotEqual, Value: "deleted", }}, LogicOp: "AND", }}, query.Filters...) } else { query.Filters = []utils.FilterGroup{{ Filters: []utils.DynamicFilter{{ Column: "status", Operator: utils.OpNotEqual, Value: "deleted", }}, LogicOp: "AND", }} } // Execute queries sequentially var total int var items []` + data.Category + `Models.` + data.Name + ` // 1. Get total count countQuery := query countQuery.Limit = 0 countQuery.Offset = 0 countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) if err != nil { return nil, 0, fmt.Errorf("failed to build count query: %w", err) } if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // 2. Get main data mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) if err != nil { return nil, 0, fmt.Errorf("failed to build main query: %w", err) } rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) if err != nil { return nil, 0, fmt.Errorf("failed to execute main query: %w", err) } defer rows.Close() for rows.Next() { item, err := h.scan` + data.Name + `(rows) if err != nil { return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows iteration error: %w", err) } return items, total, nil } ` } func generateCreateMethod(data HandlerData) string { return ` // Create` + data.Name + ` godoc // @Summary Create ` + data.NameLower + ` // @Description Creates a new ` + data.NameLower + ` record // @Tags ` + data.Name + ` // @Accept json // @Produce json // @Param request body ` + data.Category + `Models.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" // @Success 201 {object} ` + data.Category + `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/` + data.NamePlural + ` [post] func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { var req ` + data.Category + `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 := ` + data.NameLower + `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 := ` + data.Category + `Models.` + 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.Name + ` // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" // @Param request body ` + data.Category + `Models.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" // @Success 200 {object} ` + data.Category + `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/` + 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 ` + data.Category + `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 := ` + data.NameLower + `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 := ` + data.Category + `Models.` + 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.Name + ` // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" // @Success 200 {object} ` + data.Category + `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/` + 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 := ` + data.Category + `Models.` + 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.Name + ` // @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/` + 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 { var helperMethods strings.Builder // Helper methods yang selalu dibutuhkan untuk semua handlers helperMethods.WriteString(` // Optimized scanning function - selalu dibutuhkan untuk semua operasi database func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `Models.` + data.Name + `, error) { var item ` + data.Category + `Models.` + data.Name + ` // Scan into individual fields to handle nullable types properly err := rows.Scan( &item.ID, &item.Status, &item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32 &item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString &item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime &item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString &item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime &item.Name.String, &item.Name.Valid, // sql.NullString ) return item, err } // Enhanced error handling - selalu dibutuhkan untuk semua handlers 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(), }) } `) // Helper methods untuk GET operations if data.HasGet { helperMethods.WriteString(` // Database operations untuk GET by ID func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `Models.` + 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 ` + data.Category + `Models.` + 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 } `) // Helper untuk fetch dengan pagination/filter helperMethods.WriteString(` func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `Models.` + 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([]` + data.Category + `Models.` + 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) } if *verboseFlag { log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) } return items, nil } `) // Helper untuk pagination helperMethods.WriteString(` // 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 } if *verboseFlag { log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) } return limit, offset, nil } `) // Helper untuk filter (jika ada filter atau search) if data.HasFilter || data.HasSearch { helperMethods.WriteString(` func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `Models.` + data.Name + `Filter { filter := ` + data.Category + `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 } // Build WHERE clause dengan filter parameters func (h *` + data.Name + `Handler) buildWhereClause(filter ` + data.Category + `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 } `) } // Helper untuk pagination meta helperMethods.WriteString(` 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, } } `) // Helper untuk total count (dibutuhkan untuk pagination dan stats) if data.HasPagination || data.HasStats { helperMethods.WriteString(` func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `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 } `) } // Helper untuk aggregate data (stats) if data.HasStats { helperMethods.WriteString(` // Get comprehensive aggregate data dengan filter support func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + 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 } `) } } // Helper methods untuk POST operations if data.HasPost { helperMethods.WriteString(` // Database operations untuk CREATE func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) (*` + data.Category + `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, sort, user_created, date_created, user_updated, date_updated, name" row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) var item ` + data.Category + `Models.` + 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 } // 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 *` + data.Category + `Models.` + 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 *` + data.Category + `Models.` + 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) } `) } // Helper methods untuk PUT operations if data.HasPut { helperMethods.WriteString(` // Database operations untuk UPDATE func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `UpdateRequest) (*` + data.Category + `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, sort, user_created, date_created, user_updated, date_updated, name" row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) var item ` + data.Category + `Models.` + 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 } `) } // Helper methods untuk DELETE operations if data.HasDelete { helperMethods.WriteString(` // Database operations untuk DELETE 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 } `) } // Helper methods untuk DYNAMIC operations if data.HasDynamic { helperMethods.WriteString(` // fetch` + data.Name + `sDynamic executes dynamic query func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) { // Setup query builders countBuilder := utils.NewQueryBuilder("` + data.TableName + `"). SetColumnMapping(map[string]string{ // Add your column mappings here }). SetAllowedColumns([]string{ "id", "status", "sort", "user_created", "date_created", "user_updated", "date_updated", "name", // Add other allowed columns here }) mainBuilder := utils.NewQueryBuilder("` + data.TableName + `"). SetColumnMapping(map[string]string{ // Add your column mappings here }). SetAllowedColumns([]string{ "id", "status", "sort", "user_created", "date_created", "user_updated", "date_updated", "name", // Add other allowed columns here }) // Add default filter to exclude deleted records if len(query.Filters) > 0 { query.Filters = append([]utils.FilterGroup{{ Filters: []utils.DynamicFilter{{ Column: "status", Operator: utils.OpNotEqual, Value: "deleted", }}, LogicOp: "AND", }}, query.Filters...) } else { query.Filters = []utils.FilterGroup{{ Filters: []utils.DynamicFilter{{ Column: "status", Operator: utils.OpNotEqual, Value: "deleted", }}, LogicOp: "AND", }} } // Execute queries sequentially var total int var items []` + data.Category + `Models.` + data.Name + ` // 1. Get total count countQuery := query countQuery.Limit = 0 countQuery.Offset = 0 countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) if err != nil { return nil, 0, fmt.Errorf("failed to build count query: %w", err) } if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // 2. Get main data mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) if err != nil { return nil, 0, fmt.Errorf("failed to build main query: %w", err) } rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) if err != nil { return nil, 0, fmt.Errorf("failed to execute main query: %w", err) } defer rows.Close() for rows.Next() { item, err := h.scan` + data.Name + `(rows) if err != nil { return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows iteration error: %w", err) } return items, total, nil } `) } return helperMethods.String() } // ================= MODEL GENERATION ===================== func generateModelFile(data HandlerData, modelDir string) { // Tentukan nama file model modelFileName := data.NameLower + ".go" modelFilePath := filepath.Join(modelDir, modelFileName) // Periksa apakah file model sudah ada if _, err := os.Stat(modelFilePath); err == nil { // File sudah ada, skip pembuatan model fmt.Printf("Model %s already exists, skipping generation\n", data.Name) return } // Tentukan import block var importBlock, nullablePrefix string if data.Category == "models" { importBlock = `import ( "database/sql" "encoding/json" "time" ) ` } else { nullablePrefix = "models." importBlock = `import ( "` + data.ModuleName + `/internal/models" "database/sql" "encoding/json" "time" ) ` } modelContent := `package ` + data.Category + ` ` + importBlock + ` // ` + 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 ` + nullablePrefix + "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), } 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 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 ` + nullablePrefix + "MetaResponse `json:\"meta\"`" + ` Summary *` + nullablePrefix + "AggregateData `json:\"summary,omitempty\"`" + ` } ` } if data.HasPost { modelContent += ` // Request struct untuk 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 untuk 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\"`" + ` 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(modelFilePath, modelContent) fmt.Printf("Successfully generated model: %s\n", modelFileName) } // ================= 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) // Clean up duplicate routes first routesContent = cleanupDuplicateRoutes(routesContent, data) // Build import path var importPath, importAlias string if data.Category != "models" { importPath = fmt.Sprintf("%s/internal/handlers/"+data.Category, data.ModuleName) importAlias = data.Category + data.Name + "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) } } // Check if routes for this specific endpoint already exist if routeBlockExists(routesContent, data) { if *verboseFlag { fmt.Printf("โœ… Routes for %s (%s) already exist, skipping...\n", data.Name, getGroupPath(data)) } return } // Build new routes in protected group format newRoutes := generateProtectedRouteBlock(data) // Use the correct marker for insertion insertMarker := ` // ============================================================================= // PUBLISHED ROUTES // ============================================================================= ` // Find the position to insert routes if strings.Contains(routesContent, insertMarker) { // Insert after the marker block (including the newlines) markerIndex := strings.Index(routesContent, insertMarker) if markerIndex != -1 { // Find the end of the marker block (including the newlines) endOfMarker := markerIndex + len(insertMarker) // Insert the new routes after the marker routesContent = routesContent[:endOfMarker] + "\n" + newRoutes + routesContent[endOfMarker:] if *verboseFlag { fmt.Printf("๐Ÿ“ Inserted routes after PUBLISHED ROUTES marker\n") } } } else { // Fallback: try to find alternative markers alternativeMarkers := []string{ "// ============= PUBLISHED ROUTES ===============================================", "// PUBLISHED ROUTES", "return r", // End of function } inserted := false for _, marker := range alternativeMarkers { if strings.Contains(routesContent, marker) { if marker == "return r" { // Insert before the return statement routesContent = strings.Replace(routesContent, "\t"+marker, newRoutes+"\n\n\t"+marker, 1) } else { // Insert after the marker routesContent = strings.Replace(routesContent, marker, marker+"\n"+newRoutes, 1) } if *verboseFlag { fmt.Printf("๐Ÿ“ Inserted routes using alternative marker: %s\n", marker) } inserted = true break } } if !inserted { // Last resort: append at the end of the file routesContent += "\n" + newRoutes if *verboseFlag { fmt.Printf("๐Ÿ“ Appended routes at the end of file\n") } } } if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { logError("Error writing routes.go", err, *verboseFlag) return } if *verboseFlag { fmt.Printf("โœ… Updated routes.go with %s endpoints\n", data.Name) } } // routeBlockExists checks if a route block for the specific endpoint already exists func routeBlockExists(content string, data HandlerData) bool { groupPath := getGroupPath(data) handlerName := getHandlerName(data) // Build the expected route block pattern - lebih spesifik patterns := []string{ // Pattern 1: Full route block dengan comment (paling spesifik) fmt.Sprintf("// %s endpoints\n %sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", data.Name, handlerName, handlerName, data.Name, handlerName, groupPath), // Pattern 2: Handler dan Group declaration (tanpa comment) fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", handlerName, handlerName, data.Name, handlerName, groupPath), // Pattern 3: Group declaration saja (jika handler sudah ada di import) fmt.Sprintf("%sGroup := v1.Group(\"/%s\")", handlerName, groupPath), // Pattern 4: Cari kombinasi handler dan group dengan spasi yang berbeda fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name), } // Check if any of the patterns exist for _, pattern := range patterns { if strings.Contains(content, pattern) { if *verboseFlag { fmt.Printf("๐Ÿ” Found existing route pattern: %s\n", pattern) } return true } } // Additional check: look for any route with the same group path groupPattern := fmt.Sprintf("v1.Group(\"/%s\")", groupPath) if strings.Contains(content, groupPattern) { if *verboseFlag { fmt.Printf("๐Ÿ” Found existing group pattern: %s\n", groupPattern) } return true } return false } // getGroupPath returns the group path for the given data func getGroupPath(data HandlerData) string { if data.Category != "models" { return strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) } return strings.ToLower(data.Name) } // getHandlerName returns the handler name for the given data func getHandlerName(data HandlerData) string { if data.Category != "models" { return data.Category + data.Name } return strings.ToLower(data.Name) } func generateProtectedRouteBlock(data HandlerData) string { var sb strings.Builder handlerName := getHandlerName(data) groupPath := getGroupPath(data) // Komentar dan deklarasi handler & grup sb.WriteString("// ") sb.WriteString(data.Name) sb.WriteString(" endpoints\n") sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Handler := ") sb.WriteString(handlerName) sb.WriteString("Handlers.New") sb.WriteString(data.Name) sb.WriteString("Handler()\n ") sb.WriteString(handlerName) sb.WriteString("Group := v1.Group(\"/") sb.WriteString(groupPath) sb.WriteString("\")\n {\n ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString(")\n") if data.HasDynamic { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/dynamic\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Dynamic) // Route baru\n") } if data.HasSearch { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/search\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Search") sb.WriteString(data.Name) sb.WriteString("Advanced) // Route pencarian\n") } sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/:id\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("ByID)\n") if data.HasPost { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.POST(\"\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Create") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasPut { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.PUT(\"/:id\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Update") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasDelete { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.DELETE(\"/:id\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Delete") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasStats { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/stats\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Stats)\n") } sb.WriteString(" }\n") return sb.String() } func cleanupDuplicateRoutes(content string, data HandlerData) string { // Implement getGroupPath logic directly var groupPath string if data.Category != "models" { groupPath = strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) } else { groupPath = strings.ToLower(data.Name) } // Implement getHandlerName logic directly var handlerName string if data.Category != "models" { handlerName = data.Category + data.Name } else { handlerName = strings.ToLower(data.Name) } // Split content by lines for better processing lines := strings.Split(content, "\n") var cleanedLines []string inRouteBlock := false routeBlockFound := false blockStartLine := -1 for i, line := range lines { // Check if this line starts a route block for our endpoint // Look for comment pattern first if strings.Contains(line, fmt.Sprintf("// %s endpoints", data.Name)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate route block for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // Alternative pattern: look for handler declaration without comment if !inRouteBlock && strings.Contains(line, fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate handler declaration for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // Third pattern: look for group declaration (for cases without handler declaration on same line) if !inRouteBlock && strings.Contains(line, fmt.Sprintf("Group := v1.Group(\"/%s\")", groupPath)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate group declaration for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // If we're in a route block, check if we've reached the end if inRouteBlock { // Count braces to determine if we're at the end of the block openBraces := strings.Count(line, "{") closeBraces := strings.Count(line, "}") // If we have more closing braces than opening braces, we might be at the end if closeBraces > openBraces { // Check if this line contains the closing brace for the route group if strings.Contains(line, "}") && (strings.Contains(line, "Group") || strings.Contains(line, "handler")) { // This is likely the end of our route block if routeBlockFound && blockStartLine != -1 { cleanedLines = append(cleanedLines, line) inRouteBlock = false blockStartLine = -1 continue } } } // Additional check: look for standalone closing brace that might end the block if strings.TrimSpace(line) == "}" && inRouteBlock { // This might be the end of our route block if routeBlockFound && blockStartLine != -1 { cleanedLines = append(cleanedLines, line) inRouteBlock = false blockStartLine = -1 continue } } // Add the line if we're keeping this block if routeBlockFound { cleanedLines = append(cleanedLines, line) } } else { // Not in a route block, just add the line cleanedLines = append(cleanedLines, line) } } return strings.Join(cleanedLines, "\n") } 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 { logError(fmt.Sprintf("Error creating file %s", filename), err, *verboseFlag) return } if *verboseFlag { fmt.Printf("โœ… Generated: %s\n", filename) } } // Enhanced error logging function func logError(message string, err error, verbose bool) { if verbose { log.Printf("โŒ ERROR: %s - %v", message, err) } else { log.Printf("โŒ ERROR: %s", message) } } // Success logging function func logSuccess(message string, details ...string) { fmt.Printf("โœ… %s", message) for _, detail := range details { fmt.Printf(" - %s", detail) } fmt.Println() }