diff --git a/tools/general/generate-handler.go b/tools/general/generate-handler.go index bddbdcb..bd0408c 100644 --- a/tools/general/generate-handler.go +++ b/tools/general/generate-handler.go @@ -1,11 +1,15 @@ package main import ( + "flag" "fmt" + "log" "os" "path/filepath" "strings" "time" + + "gopkg.in/yaml.v2" ) // HandlerData contains template data for handler generation @@ -29,6 +33,83 @@ type HandlerData struct { 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 @@ -36,25 +117,47 @@ type PathInfo struct { FilePath string } -// parseEntityPath - Logic parsing yang diperbaiki +// 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 - parts := strings.Split(entityPath, "/") + + // 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 + + // 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], "/") @@ -64,9 +167,24 @@ func parseEntityPath(entityPath string) (*PathInfo, error) { 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{ @@ -94,23 +212,29 @@ func generateTableName(pathInfo *PathInfo) string { return "data_" + entityLower } -// createDirectories - Buat direktori sesuai struktur path +// 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 != "" { - handlerDir = filepath.Join("internal", "handlers", pathInfo.DirPath) - modelDir = filepath.Join("internal", "models", 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 + // 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 @@ -140,8 +264,279 @@ func setMethods(data *HandlerData, methods []string) { } } +// 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() { - // Validasi argument + // 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:") @@ -149,6 +544,11 @@ func main() { 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) } @@ -156,7 +556,7 @@ func main() { entityPath := strings.TrimSpace(os.Args[1]) pathInfo, err := parseEntityPath(entityPath) if err != nil { - fmt.Printf("โŒ Error parsing path: %v\n", err) + logError("Error parsing path", err, true) os.Exit(1) } @@ -171,10 +571,15 @@ func main() { // Validate methods if err := validateMethods(methods); err != nil { - fmt.Printf("โŒ %v\n", err) + 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) @@ -203,39 +608,42 @@ func main() { // Create directories handlerDir, modelDir, err := createDirectories(pathInfo) if err != nil { - fmt.Printf("โŒ Error creating directories: %v\n", err) - os.Exit(1) + 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) + updateRoutesFile(data) // Ini untuk legacy mode, masih ok karena hanya sekali per entity // Success output - fmt.Printf("โœ… Successfully generated handler: %s\n", entityName) - if pathInfo.Category != "" { - fmt.Printf("๐Ÿ“ Category: %s\n", pathInfo.Category) - } - if pathInfo.DirPath != "" { - fmt.Printf("๐Ÿ“‚ Path: %s\n", pathInfo.DirPath) - } - fmt.Printf("๐Ÿ“„ Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) - fmt.Printf("๐Ÿ“„ Model: %s\n", filepath.Join(modelDir, entityLower+".go")) - fmt.Printf("๐Ÿ—„๏ธ Table: %s\n", tableName) - fmt.Printf("๐Ÿ› ๏ธ Methods: %s\n", strings.Join(methods, ", ")) + 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 modelsImportPath string - // if data.Category != "" { - // modelsImportPath = data.ModuleName + "/internal/models/" + data.Category - // } else { - // modelsImportPath = data.ModuleName + "/internal/models" - // } - - // pakai strings.Builder biar lebih clean var handlerContent strings.Builder // Header @@ -245,15 +653,19 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n") handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n") if data.Category != "models" { - handlerContent.WriteString(` "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") + handlerContent.WriteString(data.Category + `Models "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") } - // Conditional imports + // Conditional imports based on enabled methods if data.HasDynamic || data.HasSearch { handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n") } - handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\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") @@ -281,18 +693,25 @@ func generateHandlerFile(data HandlerData, handlerDir string) { 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") - handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\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 - 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") + // 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") @@ -308,7 +727,7 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(" }\n") handlerContent.WriteString("}\n") - // Add optional methods + // Add optional methods based on enabled flags if data.HasGet { handlerContent.WriteString(generateGetMethods(data)) } @@ -331,7 +750,7 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(generateStatsMethod(data)) } - // Add helper methods + // Add helper methods - this function now handles conditional generation internally handlerContent.WriteString(generateHelperMethods(data)) // Write into file @@ -352,109 +771,109 @@ func generateGetMethods(data HandlerData) string { // @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 + `.` + data.Name + `GetResponse "Success response" +// @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 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 - } + // 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() + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() - // Execute concurrent operations - var ( - items []` + data.Category + `.` + data.Name + ` - total int - aggregateData *models.AggregateData - wg sync.WaitGroup - errChan = make(chan error, 3) - mu sync.Mutex - ) + // 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 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 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() - }() - } + // 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) + // 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 - } - } + // 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 + `.` + data.Name + `GetResponse{ - Message: "Data ` + data.Category + ` berhasil diambil", - Data: items, - Meta: meta, - } + // 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 - } + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) } // Get` + data.Name + `ByID godoc @@ -464,45 +883,45 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetByIDResponse "Success response" +// @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 - } + 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 - } + 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() + 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 - } + 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 + `.` + data.Name + `GetByIDResponse{ - Message: "` + data.Category + ` details retrieved successfully", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `GetByIDResponse{ + Message: "` + data.Category + ` details retrieved successfully", + Data: item, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -520,127 +939,276 @@ func generateDynamicMethod(data HandlerData) string { // @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 + `.` + data.Name + `GetResponse "Success response" +// @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 - } + // 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 - } + // 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() + // 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 - } + // 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 + `.` + data.Name + `GetResponse{ - Message: "Data ` + data.Category + ` berhasil diambil", - Data: items, - Meta: meta, - } + // 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) + c.JSON(http.StatusOK, response) }` } func generateSearchMethod(data HandlerData) string { return ` -// Search` + data.Name + `Advanced provides advanced search capabilities +// 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 == "" { - h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest) - return - } + // 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, + } - // Build dynamic query for search - query := utils.DynamicQuery{ - Fields: []string{"*"}, - Filters: []utils.FilterGroup{{ - Filters: []utils.DynamicFilter{ - { - Column: "status", - Operator: utils.OpNotEqual, - Value: "deleted", - }, - { - Column: "name", - Operator: utils.OpContains, - Value: searchQuery, - LogicOp: "OR", - }, - }, + // 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", - }}, - Sort: []utils.SortField{{ - Column: "date_created", - Order: "DESC", - }}, - Limit: 20, - Offset: 0, + }}, query.Filters...) + } else { + query.Filters = []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }} } - // 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 - } - } + // Execute queries sequentially + var total int + var items []` + data.Category + `Models.` + data.Name + ` - // Get database connection - dbConn, err := h.db.GetDB("postgres_satudata") + // 1. Get total count + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + + countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return + return nil, 0, fmt.Errorf("failed to build count query: %w", err) } - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } - // Execute search - items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + // 2. Get main data + mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) if err != nil { - h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) - return + return nil, 0, fmt.Errorf("failed to build main query: %w", err) } - // Build response - meta := h.calculateMeta(query.Limit, query.Offset, total) - response := ` + data.Category + `.` + data.Name + `GetResponse{ - Message: fmt.Sprintf("Search results for '%s'", searchQuery), - Data: items, - Meta: meta, + 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) } - c.JSON(http.StatusOK, response) -}` + 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 { @@ -652,51 +1220,51 @@ func generateCreateMethod(data HandlerData) string { // @Tags ` + data.Name + ` // @Accept json // @Produce json -// @Param request body ` + data.Category + `.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" -// @Success 201 {object} ` + data.Category + `.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" +// @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 + `.` + data.Name + `CreateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } + 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 - } + // 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 - } + 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() + 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 - } + // 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 - } + 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 + `.` + data.Name + `CreateResponse{ - Message: "` + data.Name + ` berhasil dibuat", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `CreateResponse{ + Message: "` + data.Name + ` berhasil dibuat", + Data: item, + } - c.JSON(http.StatusCreated, response) + c.JSON(http.StatusCreated, response) }` } @@ -710,61 +1278,61 @@ func generateUpdateMethod(data HandlerData) string { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Param request body ` + data.Category + `.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully" +// @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 - } + 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 + `.` + data.Name + `UpdateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", 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 + // 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 - } + // 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 - } + 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() + 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 - } + 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 + `.` + data.Name + `UpdateResponse{ - Message: "` + data.Name + ` berhasil diperbarui", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `UpdateResponse{ + Message: "` + data.Name + ` berhasil diperbarui", + Data: item, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -778,45 +1346,45 @@ func generateDeleteMethod(data HandlerData) string { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully" +// @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 - } + 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 - } + 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() + 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 - } + 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 + `.` + data.Name + `DeleteResponse{ - Message: "` + data.Name + ` berhasil dihapus", - ID: id, - } + response := ` + data.Category + `Models.` + data.Name + `DeleteResponse{ + Message: "` + data.Name + ` berhasil dihapus", + ID: id, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -834,573 +1402,636 @@ func generateStatsMethod(data HandlerData) string { // @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 - } + 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() + 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 - } + 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, - }) + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik ` + data.NameLower + ` berhasil diambil", + "data": aggregateData, + }) }` } func generateHelperMethods(data HandlerData) string { - helperMethods := ` + var helperMethods strings.Builder -// Database operations -func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `.` + 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 + `.` + 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 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 } -func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) (*` + data.Category + `.` + 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 + `.` + data.Name + ` - err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err) - } - - return &item, nil -} - -func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `UpdateRequest) (*` + data.Category + `.` + 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 + `.` + data.Name + ` - err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err) - } - - return &item, nil -} - -func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { - now := time.Now() - query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" - - result, err := dbConn.ExecContext(ctx, query, id, now) - if err != nil { - return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %w", err) - } - - if rowsAffected == 0 { - return sql.ErrNoRows - } - - return nil -} - -func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `.` + 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 + `.` + data.Name + `, 0, limit) - for rows.Next() { - item, err := h.scan` + data.Name + `(rows) - if err != nil { - return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err) - } - items = append(items, item) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("rows iteration error: %w", err) - } - - log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) - return items, nil -}` - - // Add dynamic fetch method if needed - if data.HasDynamic { - helperMethods += ` - -// fetchRetribusisDynamic executes dynamic query -func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `.` + data.Name + `, int, error) { - // Setup query builder - builder := utils.NewQueryBuilder("` + data.TableName + `"). - SetAllowedColumns([]string{ - "id", "status", "sort", "user_created", "date_created", - "user_updated", "date_updated", "name", - }) - - // Add default filter to exclude deleted records - query.Filters = append([]utils.FilterGroup{{ - Filters: []utils.DynamicFilter{{ - Column: "status", - Operator: utils.OpNotEqual, - Value: "deleted", - }}, - LogicOp: "AND", - }}, query.Filters...) - - // Execute concurrent queries - var ( - items [] ` + data.Category + `.` + data.Name + ` - total int - wg sync.WaitGroup - errChan = make(chan error, 2) - mu sync.Mutex - ) - - // Fetch total count - wg.Add(1) - go func() { - defer wg.Done() - countQuery := query - countQuery.Limit = 0 - countQuery.Offset = 0 - countSQL, countArgs, err := builder.BuildCountQuery(countQuery) - if err != nil { - errChan <- fmt.Errorf("failed to build count query: %w", err) - return - } - if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { - errChan <- fmt.Errorf("failed to get total count: %w", err) - return - } - }() - - // Fetch main data - wg.Add(1) - go func() { - defer wg.Done() - mainSQL, mainArgs, err := builder.BuildQuery(query) - if err != nil { - errChan <- fmt.Errorf("failed to build main query: %w", err) - return - } - - rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) - if err != nil { - errChan <- fmt.Errorf("failed to execute main query: %w", err) - return - } - defer rows.Close() - - var results []` + data.Category + `.` + data.Name + ` - for rows.Next() { - item, err := h.scan` + data.Name + `(rows) - if err != nil { - errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) - return - } - results = append(results, item) - } - - if err := rows.Err(); err != nil { - errChan <- fmt.Errorf("rows iteration error: %w", err) - return - } - - mu.Lock() - items = results - mu.Unlock() - }() - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - return nil, 0, err - } - } - - return items, total, nil -} -` - } - - helperMethods += ` -// Optimized scanning function -func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `.` + data.Name + `, error) { - var item ` + data.Category + `.` + 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 -} - -func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, total *int) error { - whereClause, args := h.buildWhereClause(filter) - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) - if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { - return fmt.Errorf("total count query failed: %w", err) - } - return nil -} - -// Get comprehensive aggregate data dengan filter support -func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter) (*models.AggregateData, error) { - aggregate := &models.AggregateData{ - ByStatus: make(map[string]int), - } - - // Build where clause untuk filter - whereClause, args := h.buildWhereClause(filter) - - // Use concurrent execution untuk performance - var wg sync.WaitGroup - var mu sync.Mutex - errChan := make(chan error, 4) - - // 1. Count by status - wg.Add(1) - go func() { - defer wg.Done() - statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) - - rows, err := dbConn.QueryContext(ctx, statusQuery, args...) - if err != nil { - errChan <- fmt.Errorf("status query failed: %w", err) - return - } - defer rows.Close() - - mu.Lock() - for rows.Next() { - var status string - var count int - if err := rows.Scan(&status, &count); err != nil { - mu.Unlock() - errChan <- fmt.Errorf("status scan failed: %w", err) - return - } - aggregate.ByStatus[status] = count - switch status { - case "active": - aggregate.TotalActive = count - case "draft": - aggregate.TotalDraft = count - case "inactive": - aggregate.TotalInactive = count - } - } - mu.Unlock() - - if err := rows.Err(); err != nil { - errChan <- fmt.Errorf("status iteration error: %w", err) - } - }() - - // 2. Get last updated time dan today statistics - wg.Add(1) - go func() { - defer wg.Done() - - // Last updated - lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause) - var lastUpdated sql.NullTime - if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { - errChan <- fmt.Errorf("last updated query failed: %w", err) - return - } - - // Today statistics - today := time.Now().Format("2006-01-02") - todayStatsQuery := fmt.Sprintf(` + "`" + ` - SELECT - SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, - SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today - FROM ` + data.TableName + ` - WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause) - - todayArgs := append(args, today) - var createdToday, updatedToday int - if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { - errChan <- fmt.Errorf("today stats query failed: %w", err) - return - } - - mu.Lock() - if lastUpdated.Valid { - aggregate.LastUpdated = &lastUpdated.Time - } - aggregate.CreatedToday = createdToday - aggregate.UpdatedToday = updatedToday - mu.Unlock() - }() - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - return nil, err - } - } - - return aggregate, nil -} - -// Enhanced error handling +// 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) + 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(), - }) + 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 - } - - log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) - return limit, offset, nil + 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 } +`) -func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `.` + data.Name + `Filter { - filter := ` + data.Category + `.` + 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 + // 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 + `.` + 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) 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, + 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 + `.` + 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 +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 + `.` + 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 +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) -}` - - return helperMethods + validator := validation.NewDuplicateValidator(dbConn) + return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier) } +`) + } -// Keep existing functions for model generation and routes... -// (The remaining functions stay the same as in the original file) + // 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 @@ -1558,12 +2189,13 @@ type ` + data.Name + `Filter struct { DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` } ` - writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) + writeFile(modelFilePath, modelContent) + fmt.Printf("Successfully generated model: %s\n", modelFileName) } // ================= ROUTES GENERATION ===================== func updateRoutesFile(data HandlerData) { - routesFile := "internal/routes/v1/routes.go" + routesFile := "../../internal/routes/v1/routes.go" content, err := os.ReadFile(routesFile) if err != nil { fmt.Printf("โš ๏ธ Could not read routes.go: %v\n", err) @@ -1574,6 +2206,9 @@ func updateRoutesFile(data HandlerData) { routesContent := string(content) + // Clean up duplicate routes first + routesContent = cleanupDuplicateRoutes(routesContent, data) + // Build import path var importPath, importAlias string if data.Category != "models" { @@ -1594,129 +2229,230 @@ func updateRoutesFile(data HandlerData) { } } + // 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) - // Insert above protected routes marker - insertMarker := "// ============= PUBLISHED ROUTES ===============================================" + // Use the correct marker for insertion + insertMarker := ` // ============================================================================= + // PUBLISHED ROUTES + // ============================================================================= +` + + // Find the position to insert routes if strings.Contains(routesContent, insertMarker) { - if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { - // Insert before the marker - routesContent = strings.Replace(routesContent, insertMarker, - newRoutes+"\n\t"+insertMarker, 1) - } else { - fmt.Printf("โœ… Routes for %s already exist, skipping...\n", data.Name) - return + // 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: insert at end of setupV1Routes function - setupFuncEnd := "\treturn r" - if strings.Contains(routesContent, setupFuncEnd) { - routesContent = strings.Replace(routesContent, setupFuncEnd, - newRoutes+"\n\n\t"+setupFuncEnd, 1) + // 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 { - fmt.Printf("Error writing routes.go: %v\n", err) + logError("Error writing routes.go", err, *verboseFlag) return } - fmt.Printf("โœ… Updated routes.go with %s endpoints\n", data.Name) + 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 { - // fmt.Printf("๐Ÿ“ Group Part: %s\n", groupPath) var sb strings.Builder - var importPath, groupPath string - if data.Category != "models" { - importPath = data.Category + data.Name - groupPath = strings.ToLower(data.Category) + "/" + data.NameLower - } else { - importPath = data.NameLower - groupPath = data.NameLower - } + 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(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler := ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handlers.New") sb.WriteString(data.Name) sb.WriteString("Handler()\n ") - sb.WriteString(importPath) - + sb.WriteString(handlerName) sb.WriteString("Group := v1.Group(\"/") sb.WriteString(groupPath) sb.WriteString("\")\n {\n ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString(")\n") if data.HasDynamic { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/dynamic\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Dynamic) // Route baru\n") } if data.HasSearch { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/search\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Search") sb.WriteString(data.Name) sb.WriteString("Advanced) // Route pencarian\n") } sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("ByID)\n") if data.HasPost { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.POST(\"\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Create") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasPut { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.PUT(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Update") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasDelete { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.DELETE(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Delete") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasStats { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/stats\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Stats)\n") @@ -1725,6 +2461,136 @@ func generateProtectedRouteBlock(data HandlerData) string { 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() @@ -1733,8 +2599,28 @@ func printRoutesSample(data HandlerData) { // ================= 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) + logError(fmt.Sprintf("Error creating file %s", filename), err, *verboseFlag) return } - fmt.Printf("โœ… Generated: %s\n", filename) + 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() } diff --git a/tools/general/service-config.yaml b/tools/general/service-config.yaml new file mode 100644 index 0000000..40944b3 --- /dev/null +++ b/tools/general/service-config.yaml @@ -0,0 +1,250 @@ +global: + module_name: "api-service" + output_dir: "internal/handlers" + enable_swagger: true + enable_logging: true + +services: + retribusi: + name: "Retribusi" + category: "retribusi" + package: "retribusi" + description: "Retribusi service for tariff and billing management" + base_url: "" + timeout: 30 + retry_count: 3 + + endpoints: + # retribusi: + # description: "Retribusi tariff management" + # handler_folder: "retribusi" + # handler_file: "retribusi.go" + # handler_name: "Retribusi" + # table_name: "data_retribusi" + # functions: + # list: + # methods: ["GET"] + # path: "/" + # get_routes: "/" + # get_path: "/" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Get retribusi list with pagination and filters" + # summary: "Get Retribusi List" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_pagination: true + # has_filter: true + # has_search: true + # has_stats: true + + # get: + # methods: ["GET"] + # path: "/:id" + # get_routes: "/:id" + # get_path: "/:id" + # model: "Retribusi" + # response_model: "RetribusiGetByIDResponse" + # description: "Get retribusi by ID" + # summary: "Get Retribusi by ID" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + + # dynamic: + # methods: ["GET"] + # path: "/dynamic" + # get_routes: "/dynamic" + # get_path: "/dynamic" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Get retribusi with dynamic filtering" + # summary: "Get Retribusi Dynamic" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_dynamic: true + + # search: + # methods: ["GET"] + # path: "/search" + # get_routes: "/search" + # get_path: "/search" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Search retribusi" + # summary: "Search Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_search: true + + # create: + # methods: ["POST"] + # path: "/" + # post_routes: "/" + # post_path: "/" + # model: "RetribusiCreateRequest" + # response_model: "RetribusiCreateResponse" + # request_model: "RetribusiCreateRequest" + # description: "Create new retribusi" + # summary: "Create Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # update: + # methods: ["PUT"] + # path: "/:id" + # put_routes: "/:id" + # put_path: "/:id" + # model: "RetribusiUpdateRequest" + # response_model: "RetribusiUpdateResponse" + # request_model: "RetribusiUpdateRequest" + # description: "Update retribusi" + # summary: "Update Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # delete: + # methods: ["DELETE"] + # path: "/:id" + # delete_routes: "/:id" + # delete_path: "/:id" + # model: "Retribusi" + # response_model: "RetribusiDeleteResponse" + # description: "Delete retribusi" + # summary: "Delete Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # stats: + # methods: ["GET"] + # path: "/stats" + # get_routes: "/stats" + # get_path: "/stats" + # model: "AggregateData" + # response_model: "AggregateData" + # description: "Get retribusi statistics" + # summary: "Get Retribusi Stats" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 180 + # has_stats: true + + # Example of another service + user: + name: "User" + category: "user" + package: "user" + description: "User management service" + base_url: "" + timeout: 30 + retry_count: 3 + + endpoints: + user: + description: "User management endpoints" + handler_folder: "retribusi" + handler_file: "user.go" + handler_name: "User" + table_name: "data_user" + functions: + list: + methods: ["GET"] + path: "/" + get_routes: "/" + get_path: "/" + model: "User" + response_model: "UserGetResponse" + description: "Get user list with pagination" + summary: "Get User List" + tags: ["User"] + require_auth: true + cache_enabled: true + enable_database: true + cache_ttl: 300 + has_pagination: true + has_filter: true + has_search: true + + get: + methods: ["GET"] + path: "/:id" + get_routes: "/:id" + get_path: "/:id" + model: "User" + response_model: "UserGetByIDResponse" + description: "Get user by ID" + summary: "Get User by ID" + tags: ["User"] + require_auth: true + cache_enabled: true + enable_database: true + cache_ttl: 300 + + create: + methods: ["POST"] + path: "/" + post_routes: "/" + post_path: "/" + model: "UserCreateRequest" + response_model: "UserCreateResponse" + request_model: "UserCreateRequest" + description: "Create new user" + summary: "Create User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0 + + update: + methods: ["PUT"] + path: "/:id" + put_routes: "/:id" + put_path: "/:id" + model: "UserUpdateRequest" + response_model: "UserUpdateResponse" + request_model: "UserUpdateRequest" + description: "Update user" + summary: "Update User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0 + + delete: + methods: ["DELETE"] + path: "/:id" + delete_routes: "/:id" + delete_path: "/:id" + model: "User" + response_model: "UserDeleteResponse" + description: "Delete user" + summary: "Delete User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0