package main import ( "flag" "fmt" "log" "os" "path/filepath" "strings" "time" "gopkg.in/yaml.v2" ) // HandlerData contains template data for handler generation type HandlerData struct { Name string NameLower string NamePlural string Category string // Untuk backward compatibility (bagian pertama) DirPath string // Path direktori lengkap ModuleName string TableName string TableSchema []ColumnConfig // Untuk penyimpanan schema Relationships []RelationshipConfig // Untuk menyimpan relasi FieldGroups map[string][]string // Untuk menyimpan field groups HasGet bool HasPost bool HasPut bool HasDelete bool HasStats bool HasDynamic bool HasSearch bool HasFilter bool HasPagination bool Timestamp string Endpoints map[string]EndpointConfig // Menyimpan semua endpoint } // Config represents the YAML configuration structure type Config 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"` Database DatabaseConfig `yaml:"database"` } // DatabaseConfig represents database configuration type DatabaseConfig struct { DefaultConnection string `yaml:"default_connection"` TimeoutSeconds int `yaml:"timeout_seconds"` } // 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"` TableName string `yaml:"table_name"` Schema SchemaConfig `yaml:"schema"` Relationships []RelationshipConfig `yaml:"relationships"` FieldGroups map[string][]string `yaml:"field_groups"` Endpoints map[string]EndpointConfig `yaml:"endpoints"` } // SchemaConfig represents a schema configuration type SchemaConfig struct { Columns []ColumnConfig `yaml:"columns"` } // ColumnConfig represents a column configuration type ColumnConfig struct { Name string `yaml:"name"` Type string `yaml:"type"` Nullable bool `yaml:"nullable,omitempty"` GoType string `yaml:"go_type,omitempty"` // Untuk override tipe Go secara manual PrimaryKey bool `yaml:"primary_key,omitempty"` Searchable bool `yaml:"searchable,omitempty"` // Menandai kolom yang dapat dicari Unique bool `yaml:"unique,omitempty"` // Menandai kolom yang harus unik SystemField bool `yaml:"system_field,omitempty"` // Menandai kolom sistem (created_at, dll) Description string `yaml:"description,omitempty"` Validation string `yaml:"validation,omitempty"` } // RelationshipConfig represents a relationship configuration type RelationshipConfig struct { Name string `yaml:"name"` Table string `yaml:"table"` ForeignKey string `yaml:"foreign_key"` LocalKey string `yaml:"local_key"` Columns []ColumnConfig `yaml:"columns"` } // EndpointConfig represents an endpoint configuration type EndpointConfig struct { HandlerFolder string `yaml:"handler_folder"` HandlerFile string `yaml:"handler_file"` Methods []string `yaml:"methods"` Path string `yaml:"path"` Description string `yaml:"description"` Summary string `yaml:"summary"` Tags []string `yaml:"tags"` RequireAuth bool `yaml:"require_auth"` CacheEnabled bool `yaml:"cache_enabled"` 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"` Fields string `yaml:"fields"` ResponseModel string `yaml:"response_model"` RequestModel string `yaml:"request_model,omitempty"` SoftDelete bool `yaml:"soft_delete,omitempty"` } type PathInfo struct { Category string EntityName string DirPath string FilePath string } // Global variables for command line flags var ( forceFlag = flag.Bool("force", false, "Force overwrite existing files") verboseFlag = flag.Bool("verbose", false, "Enable verbose output") configFlag = flag.String("config", "", "Specify config file path") ) // Global file skip function var shouldSkipExistingFile func(filePath string, fileType string) bool // Global excluded fields var excludedFields = map[string]bool{ "id": true, "date_created": true, "date_updated": true, "user_created": true, "user_updated": true, } // parseEntityPath - Enhanced logic parsing dengan validasi lebih baik func parseEntityPath(entityPath string) (*PathInfo, error) { if strings.TrimSpace(entityPath) == "" { return nil, fmt.Errorf("entity path cannot be empty") } var pathInfo PathInfo // Clean path untuk menghapus leading/trailing slashes cleanedPath := strings.Trim(entityPath, "/") parts := strings.Split(cleanedPath, "/") // Validasi minimal 1 bagian (file saja) dan maksimal 4 if len(parts) < 1 || len(parts) > 4 { return nil, fmt.Errorf("invalid path format: use up to 4 levels like 'level1/level2/level3/entity'") } // Validasi bagian kosong dan karakter tidak valid for i, part := range parts { if strings.TrimSpace(part) == "" { return nil, fmt.Errorf("empty path segment at position %d", i+1) } // Validasi karakter untuk keamanan if !isValidPathSegment(part) { return nil, fmt.Errorf("invalid characters in path segment '%s' at position %d", part, i+1) } } pathInfo.EntityName = parts[len(parts)-1] if len(parts) > 1 { pathInfo.Category = parts[len(parts)-2] pathInfo.DirPath = strings.Join(parts[:len(parts)-1], "/") pathInfo.FilePath = pathInfo.DirPath + "/" + strings.ToLower(pathInfo.EntityName) + ".go" } else { pathInfo.Category = "models" pathInfo.DirPath = "" pathInfo.FilePath = strings.ToLower(pathInfo.EntityName) + ".go" } return &pathInfo, nil } // Validasi karakter untuk path segment func isValidPathSegment(segment string) bool { // Hanya izinkan alphanumeric, underscore, dan dash for _, char := range segment { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-') { return false } } return true } // validateMethods - Validasi method yang diinput func validateMethods(methods []string) error { validMethods := map[string]bool{ "get": true, "post": true, "put": true, "delete": true, "stats": true, "dynamic": true, "search": true, } for _, method := range methods { if !validMethods[strings.ToLower(method)] { return fmt.Errorf("invalid method: %s. Valid methods: get, post, put, delete, stats, dynamic, search", method) } } return nil } // generateTableName - Generate table name berdasarkan path lengkap func generateTableName(pathInfo *PathInfo) string { entityLower := strings.ToLower(pathInfo.EntityName) if pathInfo.DirPath != "" { // Replace "/" dengan "_" untuk table name pathForTable := strings.ReplaceAll(pathInfo.DirPath, "/", "_") return "data_" + pathForTable + "_" + entityLower } return "data_" + entityLower } // createDirectories - Enhanced directory creation dengan better error handling func createDirectories(pathInfo *PathInfo) (string, string, error) { var handlerDir, modelDir string // Support nested directories lebih baik if pathInfo.DirPath != "" { // Normalize path untuk memastikan konsistensi normalizedPath := filepath.Clean(pathInfo.DirPath) handlerDir = filepath.Join("internal", "handlers", normalizedPath) modelDir = filepath.Join("internal", "models", normalizedPath) } else { handlerDir = filepath.Join("internal", "handlers") modelDir = filepath.Join("internal", "models") } // Create directories dengan permission yang tepat for _, dir := range []string{handlerDir, modelDir} { if err := os.MkdirAll(dir, 0755); err != nil { return "", "", fmt.Errorf("failed to create directory %s: %v", dir, err) } if *verboseFlag { fmt.Printf("๐Ÿ“ Created directory: %s\n", dir) } } return handlerDir, modelDir, nil } // setMethods - Set method flags berdasarkan input func setMethods(data *HandlerData, methods []string) { methodMap := map[string]*bool{ "get": &data.HasGet, "post": &data.HasPost, "put": &data.HasPut, "delete": &data.HasDelete, "stats": &data.HasStats, "dynamic": &data.HasDynamic, "search": &data.HasSearch, } for _, method := range methods { if flag, exists := methodMap[strings.ToLower(method)]; exists { *flag = true } } // Always add stats if we have get if data.HasGet { data.HasStats = true } } // Helper function untuk remove duplicate methods func removeDuplicateMethods(methods []string) []string { seen := make(map[string]bool) result := []string{} for _, method := range methods { lowerMethod := strings.ToLower(method) if !seen[lowerMethod] { seen[lowerMethod] = true result = append(result, method) } } return result } // Validasi apakah file sudah ada dan harus di-skip func defaultShouldSkipExistingFile(filePath string, fileType string) bool { if *forceFlag { if *verboseFlag { fmt.Printf("๐Ÿ”„ Force mode enabled, overwriting: %s\n", filePath) } return false } if _, err := os.Stat(filePath); err == nil { fmt.Printf("โš ๏ธ %s file already exists: %s\n", strings.Title(fileType), filePath) if *verboseFlag { fmt.Printf(" Use --force to overwrite\n") } return true } return false } func loadConfig(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var config Config if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse YAML config: %w", err) } return &config, nil } // getDefaultTableSchema returns a basic table schema if none is provided func getDefaultTableSchema() []ColumnConfig { return []ColumnConfig{ {Name: "id", Type: "uuid", Nullable: false, GoType: "string", PrimaryKey: true}, {Name: "status", Type: "varchar", Nullable: false, GoType: "string"}, {Name: "sort", Type: "int4", Nullable: true, GoType: "int32"}, {Name: "user_created", Type: "varchar", Nullable: true, GoType: "string", SystemField: true}, {Name: "date_created", Type: "timestamp", Nullable: true, GoType: "time.Time", SystemField: true}, {Name: "user_updated", Type: "varchar", Nullable: true, GoType: "string", SystemField: true}, {Name: "date_updated", Type: "timestamp", Nullable: true, GoType: "time.Time", SystemField: true}, {Name: "name", Type: "varchar", Nullable: true, GoType: "string", Searchable: true}, } } // generateFromServicesConfig - RESTRUCTURED untuk agreggasi methods func generateFromServicesConfig(config *Config) { for serviceName, service := range config.Services { if *verboseFlag { fmt.Printf("๐Ÿ”ง Processing service: %s\n", serviceName) } // Parse entity path dari service name pathInfo, err := parseEntityPath(serviceName) if err != nil { logError(fmt.Sprintf("Error parsing entity path '%s'", serviceName), err, *verboseFlag) continue } // Override category dari service config if service.Category != "" { pathInfo.Category = service.Category } // Set directory path dari handler_folder jika specified if len(service.Endpoints) > 0 { for _, endpoint := range service.Endpoints { if endpoint.HandlerFolder != "" { pathInfo.DirPath = endpoint.HandlerFolder break } } } // AGGREGATE semua methods dari semua endpoints var allMethods []string var endpointConfigs = make(map[string]EndpointConfig) // Inisialisasi map for endpointName, endpoint := range service.Endpoints { if *verboseFlag { fmt.Printf(" ๐Ÿ“ Processing endpoint: %s\n", endpointName) } // Tambahkan methods dari endpoint ini allMethods = append(allMethods, endpoint.Methods...) endpointConfigs[endpointName] = endpoint // Simpan endpoint ke map } // 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 service '%s'", serviceName), err, *verboseFlag) continue } // Override table name jika specified tableName := service.TableName if tableName == "" { tableName = generateTableName(pathInfo) } // Get table schema from service var tableSchema []ColumnConfig if len(service.Schema.Columns) > 0 { tableSchema = service.Schema.Columns } else { // Use default schema tableSchema = getDefaultTableSchema() } // Get relationships from service var relationships []RelationshipConfig if len(service.Relationships) > 0 { relationships = service.Relationships } // Get field groups from service var fieldGroups map[string][]string if len(service.FieldGroups) > 0 { fieldGroups = service.FieldGroups } // 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, TableSchema: tableSchema, Relationships: relationships, FieldGroups: fieldGroups, Endpoints: endpointConfigs, // Gunakan map yang sudah diinisialisasi Timestamp: time.Now().Format("2006-01-02 15:04:05"), } // Set methods berdasarkan aggregated methods setMethods(&data, allMethods) // Set flags berdasarkan endpoint configs for _, endpoint := range endpointConfigs { if endpoint.HasPagination { data.HasPagination = true } if endpoint.HasFilter { data.HasFilter = true } if endpoint.HasSearch { data.HasSearch = true } if endpoint.HasStats { data.HasStats = true } if endpoint.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 len(data.TableSchema) > 0 { if shouldSkipExistingFile(modelPath, "model") { fmt.Printf("โš ๏ธ Skipping model generation: %s\n", modelPath) } else { generateModelFile(data, modelDir) // Memanggil fungsi baru } } else { fmt.Printf("โš ๏ธ Skipping model generation for '%s' because no schema is defined in the config.\n", entityName) } // Generate files (SEKALI SAJA per service) generateHandlerFile(data, handlerDir) // HANYA UPDATE ROUTES SEKALI PER SERVICE setelah semua endpoint di-aggregate updateRoutesFile(data) // Success output logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), fmt.Sprintf("Category: %s", pathInfo.Category), fmt.Sprintf("Path: %s", pathInfo.DirPath), fmt.Sprintf("Handler: %s", handlerPath), fmt.Sprintf("Model: %s", modelPath), fmt.Sprintf("Table: %s", tableName), fmt.Sprintf("Methods: %s", strings.Join(allMethods, ", ")), ) } } func main() { // Parse command line flags flag.Parse() // Set global file skip function shouldSkipExistingFile = defaultShouldSkipExistingFile // Determine config file path configPath := "services-config.yaml" if *configFlag != "" { configPath = *configFlag } // Check for services-config.yaml first (new format) servicesConfig, err := loadConfig(configPath) if err == nil { // Use services config if *verboseFlag { fmt.Printf("๐Ÿ“„ Using services configuration from %s\n", configPath) } generateFromServicesConfig(servicesConfig) return } // No config files found, fallback to command line arguments fmt.Printf("โš ๏ธ No config file found (%s), falling back to command line arguments\n", configPath) if len(os.Args) < 2 { fmt.Println("Usage: go run generate-handler.go [path/]entity [methods]") fmt.Println("Examples:") fmt.Println(" go run generate-handler.go product get post put delete") fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search") fmt.Println(" go run generate-handler.go product/category/subcategory/item get post") fmt.Println("\nSupported methods: get, post, put, delete, stats, dynamic, search") fmt.Println("\nAlternatively, create a services-config.yaml file with configurations.") fmt.Println("\nFlags:") fmt.Println(" --force Force overwrite existing files") fmt.Println(" --verbose Enable verbose output") fmt.Println(" --config Specify config file path") os.Exit(1) } // Parse entity path entityPath := strings.TrimSpace(os.Args[1]) pathInfo, err := parseEntityPath(entityPath) if err != nil { logError("Error parsing path", err, true) os.Exit(1) } // Parse methods var methods []string if len(os.Args) > 2 { methods = os.Args[2:] } else { // Default methods with advanced features methods = []string{"get", "post", "put", "delete", "dynamic", "search"} } // Validate methods if err := validateMethods(methods); err != nil { logError("Method validation failed", err, true) os.Exit(1) } // Generate for single entity generateForEntity(pathInfo, methods) } func generateForEntity(pathInfo *PathInfo, methods []string) { // Format names entityName := strings.Title(pathInfo.EntityName) // PascalCase entity name entityLower := strings.ToLower(pathInfo.EntityName) entityPlural := entityLower + "s" // Generate table name tableName := generateTableName(pathInfo) // Create HandlerData data := HandlerData{ Name: entityName, NameLower: entityLower, NamePlural: entityPlural, Category: pathInfo.Category, DirPath: pathInfo.DirPath, ModuleName: "api-service", TableName: tableName, TableSchema: getDefaultTableSchema(), // Use default schema if not provided HasPagination: true, HasFilter: true, Timestamp: time.Now().Format("2006-01-02 15:04:05"), } // Set methods setMethods(&data, methods) // Create directories handlerDir, modelDir, err := createDirectories(pathInfo) if err != nil { logError("Error creating directories", err, *verboseFlag) return } // CHECK existing files sebelum generate handlerPath := filepath.Join(handlerDir, entityLower+".go") modelPath := filepath.Join(modelDir, entityLower+".go") if shouldSkipExistingFile(handlerPath, "handler") { fmt.Printf("โš ๏ธ Skipping handler generation: %s\n", handlerPath) return } if shouldSkipExistingFile(modelPath, "model") { fmt.Printf("โš ๏ธ Skipping model generation: %s\n", modelPath) return } // Generate files generateHandlerFile(data, handlerDir) generateModelFile(data, modelDir) updateRoutesFile(data) // Ini untuk legacy mode, masih ok karena hanya sekali per entity // Success output logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), fmt.Sprintf("Category: %s", pathInfo.Category), fmt.Sprintf("Path: %s", pathInfo.DirPath), fmt.Sprintf("Handler: %s", handlerPath), fmt.Sprintf("Model: %s", modelPath), fmt.Sprintf("Table: %s", tableName), fmt.Sprintf("Methods: %s", strings.Join(methods, ", ")), ) } // ================= HANDLER GENERATION ===================== func generateHandlerFile(data HandlerData, handlerDir string) { var handlerContent strings.Builder // Header handlerContent.WriteString("package handlers\n\n") handlerContent.WriteString("import (\n") handlerContent.WriteString(` "` + data.ModuleName + `/internal/config"` + "\n") handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n") handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n") if data.Category != "models" { handlerContent.WriteString(data.Category + `Models "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") } // Conditional imports based on enabled methods if data.HasDynamic || data.HasSearch { handlerContent.WriteString(` queryUtils "` + data.ModuleName + `/internal/utils/query"` + "\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(` "` + data.ModuleName + `/pkg/logger"` + "\n") handlerContent.WriteString(` "context"` + "\n") handlerContent.WriteString(` "database/sql"` + "\n") handlerContent.WriteString(` "fmt"` + "\n") handlerContent.WriteString(` "net/http"` + "\n") handlerContent.WriteString(` "strconv"` + "\n") handlerContent.WriteString(` "strings"` + "\n") handlerContent.WriteString(` "sync"` + "\n") handlerContent.WriteString(` "time"` + "\n\n") handlerContent.WriteString(` "github.com/gin-gonic/gin"` + "\n") handlerContent.WriteString(` "github.com/go-playground/validator/v10"` + "\n") handlerContent.WriteString(` "github.com/jmoiron/sqlx"` + "\n") handlerContent.WriteString(` "github.com/lib/pq"` + "\n") handlerContent.WriteString(")\n\n") // Global initialization with caching handlerContent.WriteString("// =============================================================================\n") handlerContent.WriteString("// GLOBAL INITIALIZATION & VALIDATION\n") handlerContent.WriteString("// =============================================================================\n\n") handlerContent.WriteString("var (\n") handlerContent.WriteString(" db database.Service\n") handlerContent.WriteString(" once sync.Once\n") handlerContent.WriteString(" validate *validator.Validate\n") handlerContent.WriteString(")\n\n") handlerContent.WriteString("// Initialize the database connection and validator once\n") handlerContent.WriteString("func init() {\n") handlerContent.WriteString(" once.Do(func() {\n") handlerContent.WriteString(" db = database.New(config.LoadConfig())\n") handlerContent.WriteString(" validate = validator.New()\n") if data.HasPost { handlerContent.WriteString(" validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") } handlerContent.WriteString(" if db == nil {\n") handlerContent.WriteString(" logger.Fatal(\"Failed to initialize database connection\")\n") handlerContent.WriteString(" }\n") handlerContent.WriteString(" })\n") handlerContent.WriteString("}\n\n") 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") } // Cache implementation handlerContent.WriteString("// =============================================================================\n") handlerContent.WriteString("// CACHE IMPLEMENTATION\n") handlerContent.WriteString("// =============================================================================\n\n") handlerContent.WriteString("// CacheEntry represents an entry in the cache\n") handlerContent.WriteString("type CacheEntry struct {\n") handlerContent.WriteString(" Data interface{}\n") handlerContent.WriteString(" ExpiresAt time.Time\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// IsExpired checks if the cache entry has expired\n") handlerContent.WriteString("func (e *CacheEntry) IsExpired() bool {\n") handlerContent.WriteString(" return time.Now().After(e.ExpiresAt)\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// InMemoryCache implements a simple in-memory cache with TTL\n") handlerContent.WriteString("type InMemoryCache struct {\n") handlerContent.WriteString(" items sync.Map\n") handlerContent.WriteString(" mu sync.RWMutex\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// NewInMemoryCache creates a new in-memory cache\n") handlerContent.WriteString("func NewInMemoryCache() *InMemoryCache {\n") handlerContent.WriteString(" return &InMemoryCache{}\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// Get retrieves an item from the cache\n") handlerContent.WriteString("func (c *InMemoryCache) Get(key string) (interface{}, bool) {\n") handlerContent.WriteString(" val, ok := c.items.Load(key)\n") handlerContent.WriteString(" if !ok {\n") handlerContent.WriteString(" return nil, false\n") handlerContent.WriteString(" }\n\n") handlerContent.WriteString(" entry, ok := val.(*CacheEntry)\n") handlerContent.WriteString(" if !ok || entry.IsExpired() {\n") handlerContent.WriteString(" c.items.Delete(key)\n") handlerContent.WriteString(" return nil, false\n") handlerContent.WriteString(" }\n\n") handlerContent.WriteString(" return entry.Data, true\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// Set stores an item in the cache with a TTL\n") handlerContent.WriteString("func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) {\n") handlerContent.WriteString(" entry := &CacheEntry{\n") handlerContent.WriteString(" Data: value,\n") handlerContent.WriteString(" ExpiresAt: time.Now().Add(ttl),\n") handlerContent.WriteString(" }\n") handlerContent.WriteString(" c.items.Store(key, entry)\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// Delete removes an item from the cache\n") handlerContent.WriteString("func (c *InMemoryCache) Delete(key string) {\n") handlerContent.WriteString(" c.items.Delete(key)\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// DeleteByPrefix removes all items with a specific prefix\n") handlerContent.WriteString("func (c *InMemoryCache) DeleteByPrefix(prefix string) {\n") handlerContent.WriteString(" c.items.Range(func(key, value interface{}) bool {\n") handlerContent.WriteString(" if keyStr, ok := key.(string); ok && strings.HasPrefix(keyStr, prefix) {\n") handlerContent.WriteString(" c.items.Delete(key)\n") handlerContent.WriteString(" }\n") handlerContent.WriteString(" return true\n") handlerContent.WriteString(" })\n") handlerContent.WriteString("}\n\n") // Handler struct handlerContent.WriteString("// =============================================================================\n") handlerContent.WriteString("// " + data.Name + " HANDLER STRUCT\n") handlerContent.WriteString("// =============================================================================\n\n") handlerContent.WriteString("// " + data.Name + "Handler handles " + data.NameLower + " services\n") handlerContent.WriteString("type " + data.Name + "Handler struct {\n") handlerContent.WriteString(" db database.Service\n") handlerContent.WriteString(" queryBuilder *queryUtils.QueryBuilder\n") handlerContent.WriteString(" validator *validation.DynamicValidator\n") handlerContent.WriteString(" cache *InMemoryCache\n") handlerContent.WriteString("}\n\n") handlerContent.WriteString("// New" + data.Name + "Handler creates a new " + data.Name + "Handler with a pre-configured QueryBuilder\n") handlerContent.WriteString("func New" + data.Name + "Handler() *" + data.Name + "Handler {\n") // Generate allowed columns from table schema var allowedColumns []string for _, col := range data.TableSchema { allowedColumns = append(allowedColumns, col.Name) } // Add relationship columns to allowed columns for _, rel := range data.Relationships { for _, col := range rel.Columns { allowedColumns = append(allowedColumns, col.Name) } } handlerContent.WriteString(" // Initialize QueryBuilder with allowed columns list for security.\n") handlerContent.WriteString(" queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).\n") handlerContent.WriteString(" SetAllowedColumns([]string{\n") for i, col := range allowedColumns { if i < len(allowedColumns)-1 { handlerContent.WriteString(" \"" + col + "\",\n") } else { handlerContent.WriteString(" \"" + col + "\",\n") } } handlerContent.WriteString(" })\n\n") handlerContent.WriteString(" return &" + data.Name + "Handler{\n") handlerContent.WriteString(" db: db,\n") handlerContent.WriteString(" queryBuilder: queryBuilder,\n") handlerContent.WriteString(" validator: validation.NewDynamicValidator(queryBuilder),\n") handlerContent.WriteString(" cache: NewInMemoryCache(),\n") handlerContent.WriteString(" }\n") handlerContent.WriteString("}\n\n") // Handler endpoints handlerContent.WriteString("// =============================================================================\n") handlerContent.WriteString("// HANDLER ENDPOINTS\n") handlerContent.WriteString("// =============================================================================\n\n") // Generate all endpoints in one file for endpointName, endpoint := range data.Endpoints { if *verboseFlag { fmt.Printf(" ๐Ÿ“ Generating endpoint: %s\n", endpointName) } // Generate endpoint based on its methods for _, method := range endpoint.Methods { switch strings.ToLower(method) { case "get": primaryKey := findPrimaryKey(data.TableSchema) if endpoint.Path == "/:"+primaryKey { handlerContent.WriteString(generateGetByIDMethod(data, endpoint)) } else if endpoint.Path == "/dynamic" { handlerContent.WriteString(generateDynamicMethod(data, endpoint)) } else if endpoint.Path == "/search" { handlerContent.WriteString(generateSearchMethod(data, endpoint)) } else if endpoint.Path == "/stats" { handlerContent.WriteString(generateStatsMethod(data, endpoint)) } else if endpoint.Path == "/by-location" { handlerContent.WriteString(generateByLocationMethod(data, endpoint)) } else if endpoint.Path == "/by-age" { handlerContent.WriteString(generateByAgeMethod(data, endpoint)) } else { handlerContent.WriteString(generateGetMethod(data, endpoint)) } case "post": handlerContent.WriteString(generateCreateMethod(data, endpoint)) case "put": handlerContent.WriteString(generateUpdateMethod(data, endpoint)) case "delete": handlerContent.WriteString(generateDeleteMethod(data, endpoint)) } } } // Helper methods handlerContent.WriteString("// =============================================================================\n") handlerContent.WriteString("// HELPER FUNCTIONS\n") handlerContent.WriteString("// =============================================================================\n\n") handlerContent.WriteString(generateHelperMethodsWithCache(data)) // Write into file writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent.String()) } // generateGetMethod - Template untuk GET method dengan cache func generateGetMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Get" + data.Name + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") // Add parameters based on endpoint configuration if endpoint.HasPagination { methodContent.WriteString("// @Param limit query int false \"Limit (max 100)\" default(10)\n") methodContent.WriteString("// @Param offset query int false \"Offset\" default(0)\n") } if endpoint.HasFilter { statusColumn := findStatusColumn(data.TableSchema) methodContent.WriteString("// @Param " + statusColumn + " query string false \"Filter by status\"\n") } if endpoint.HasSearch { methodContent.WriteString("// @Param search query string false \"Search in multiple fields\"\n") } methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Success response\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "(c *gin.Context) {\n") methodContent.WriteString(" // Increase timeout for complex queries\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Get the fields for this endpoint from the configuration fields := getFieldsForEndpoint(data, endpoint.Fields) methodContent.WriteString(" // Use the core fetch" + data.Name + "sDynamic function for all data retrieval logic.\n") methodContent.WriteString(" query := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") // Generate select fields based on the endpoint configuration for _, field := range fields { methodContent.WriteString(" {Expression: \"" + field + "\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Sort: []queryUtils.SortField{{Column: \"date_created\", Order: \"DESC\"}},\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(fields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" query.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(fields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Parse pagination\n") methodContent.WriteString(" if limit, err := strconv.Atoi(c.DefaultQuery(\"limit\", \"10\")); err == nil && limit > 0 && limit <= 100 {\n") methodContent.WriteString(" query.Limit = limit\n") methodContent.WriteString(" }\n") methodContent.WriteString(" if offset, err := strconv.Atoi(c.DefaultQuery(\"offset\", \"0\")); err == nil && offset >= 0 {\n") methodContent.WriteString(" query.Offset = offset\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Parse simple filters\n") methodContent.WriteString(" var filters []queryUtils.DynamicFilter\n") // Add status filter if status column exists statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" if " + statusColumn + " := c.Query(\"" + statusColumn + "\"); " + statusColumn + " != \"\" && models.IsValidStatus(" + statusColumn + ") {\n") methodContent.WriteString(" filters = append(filters, queryUtils.DynamicFilter{Column: \"" + statusColumn + "\", Operator: queryUtils.OpEqual, Value: " + statusColumn + "})\n") methodContent.WriteString(" }\n") } methodContent.WriteString(" \n") methodContent.WriteString(" // Optimize query search with caching\n") methodContent.WriteString(" search := c.Query(\"search\")\n") methodContent.WriteString(" var searchFilters []queryUtils.DynamicFilter\n") methodContent.WriteString(" var cacheKey string\n") methodContent.WriteString(" var useCache bool\n\n") methodContent.WriteString(" // Initialize searchFilters before using it in the cache hit section\n") methodContent.WriteString(" if search != \"\" {\n") methodContent.WriteString(" // Limit search length to prevent slow queries\n") methodContent.WriteString(" if len(search) > 50 {\n") methodContent.WriteString(" search = search[:50]\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Generate cache key for search\n") methodContent.WriteString(" cacheKey = fmt.Sprintf(\"" + data.NameLower + ":search:%s:%d:%d\", search, query.Limit, query.Offset)\n\n") methodContent.WriteString(" // Initialize searchFilters here\n") methodContent.WriteString(" searchFilters = []queryUtils.DynamicFilter{\n") // Add searchable columns based on table schema searchableColumns := findSearchableColumns(data.TableSchema) for _, col := range searchableColumns { methodContent.WriteString(" {Column: \"" + col + "\", Operator: queryUtils.OpILike, Value: \"%\" + search + \"%\"},\n") } methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Try to get from cache first\n") methodContent.WriteString(" if cachedData, found := h.cache.Get(cacheKey); found {\n") methodContent.WriteString(" logger.Info(\"Cache hit for search\", map[string]interface{}{\"search\": search, \"cache_key\": cacheKey})\n\n") methodContent.WriteString(" // Convert from interface{} to expected type\n") methodContent.WriteString(" " + data.NamePlural + ", ok := cachedData.([]" + data.Category + "Models." + data.Name + ")\n") methodContent.WriteString(" if !ok {\n") methodContent.WriteString(" logger.Error(\"Failed to convert cached data\", map[string]interface{}{\"cache_key\": cacheKey})\n") methodContent.WriteString(" } else {\n") methodContent.WriteString(" // If requested, get aggregation data\n") methodContent.WriteString(" var aggregateData *models.AggregateData\n") methodContent.WriteString(" if c.Query(\"include_summary\") == \"true\" {\n") methodContent.WriteString(" // Build full filter groups for aggregate data (including search filters)\n") methodContent.WriteString(" fullFilterGroups := []queryUtils.FilterGroup{\n") methodContent.WriteString(" {Filters: searchFilters, LogicOp: \"OR\"},\n") methodContent.WriteString(" }\n") methodContent.WriteString(" if len(filters) > 0 {\n") methodContent.WriteString(" fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: \"AND\"})\n") methodContent.WriteString(" }\n") methodContent.WriteString(" aggregateData, err = h.getAggregateData(ctx, dbConn, fullFilterGroups)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get aggregate data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" meta := h.calculateMeta(query.Limit, query.Offset, len(" + data.NamePlural + "))\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"Data " + data.NameLower + " berhasil diambil (dari cache)\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if aggregateData != nil {\n") methodContent.WriteString(" response.Summary = aggregateData\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // If not in cache, mark for saving after query\n") methodContent.WriteString(" useCache = true\n\n") methodContent.WriteString(" // If there's search, create OR filter group\n") methodContent.WriteString(" query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: \"OR\"})\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Add other filters (if any) as AND group\n") methodContent.WriteString(" if len(filters) > 0 {\n") methodContent.WriteString(" query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: \"AND\"})\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" " + data.NamePlural + ", total, err := h.fetch" + data.Name + "sDynamic(ctx, dbConn, query)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to fetch data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Save search results to cache if there's a search parameter\n") methodContent.WriteString(" if useCache && len(" + data.NamePlural + ") > 0 {\n") methodContent.WriteString(" h.cache.Set(cacheKey, " + data.NamePlural + ", 15*time.Minute) // Cache for 15 minutes\n") methodContent.WriteString(" logger.Info(\"Cached search results\", map[string]interface{}{\"search\": search, \"cache_key\": cacheKey, \"count\": len(" + data.NamePlural + ")})\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // If requested, get aggregation data\n") methodContent.WriteString(" var aggregateData *models.AggregateData\n") methodContent.WriteString(" if c.Query(\"include_summary\") == \"true\" {\n") methodContent.WriteString(" aggregateData, err = h.getAggregateData(ctx, dbConn, query.Filters)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get aggregate data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" meta := h.calculateMeta(query.Limit, query.Offset, total)\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"Data " + data.NameLower + " berhasil diambil\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if aggregateData != nil {\n") methodContent.WriteString(" response.Summary = aggregateData\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateGetByIDMethod - Template untuk GET by ID method dengan cache func generateGetByIDMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" // Default fallback } methodContent.WriteString("// Get" + data.Name + "By" + snakeToPascal(primaryKey) + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param " + primaryKey + " path string true \"" + data.Name + " " + strings.ToUpper(primaryKey) + "\"\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Success response\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Invalid ID format\"\n") methodContent.WriteString("// @Failure 404 {object} models.ErrorResponse \"" + data.Name + " not found\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "By" + snakeToPascal(primaryKey) + "(c *gin.Context) {\n") methodContent.WriteString(" " + primaryKey + " := c.Param(\"" + primaryKey + "\")\n") methodContent.WriteString(" if " + primaryKey + " == \"\" {\n") methodContent.WriteString(" h.respondError(c, \"Invalid " + strings.ToUpper(primaryKey) + " format\", fmt.Errorf(\"" + primaryKey + " cannot be empty\"), http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Try to get from cache first\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":" + primaryKey + ":%s\", " + primaryKey + ")\n") methodContent.WriteString(" if cachedData, found := h.cache.Get(cacheKey); found {\n") methodContent.WriteString(" logger.Info(\"Cache hit for " + primaryKey + "\", map[string]interface{}{\"" + primaryKey + "\": " + primaryKey + ", \"cache_key\": cacheKey})\n\n") methodContent.WriteString(" // Convert from interface{} to expected type\n") methodContent.WriteString(" if cached" + data.Name + ", ok := cachedData.(" + data.Category + "Models." + data.Name + "); ok {\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"" + data.Name + " details retrieved successfully (dari cache)\",\n") methodContent.WriteString(" Data: &cached" + data.Name + ",\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Get the fields for this endpoint from the configuration fields := getFieldsForEndpoint(data, endpoint.Fields) methodContent.WriteString(" dynamicQuery := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") // Generate select fields based on the endpoint configuration for _, field := range fields { methodContent.WriteString(" {Expression: \"" + field + "\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: " + primaryKey + "},\n") // Add status filter if status column exists statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" {Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Limit: 1,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(fields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" dynamicQuery.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(fields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" var data" + data.Name + " " + data.Category + "Models." + data.Name + "\n") methodContent.WriteString(" err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &data" + data.Name + ")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" if err == sql.ErrNoRows {\n") methodContent.WriteString(" h.respondError(c, \"" + data.Name + " not found\", err, http.StatusNotFound)\n") methodContent.WriteString(" } else {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get " + data.NameLower + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" }\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Save to cache\n") methodContent.WriteString(" h.cache.Set(cacheKey, data" + data.Name + ", 30*time.Minute) // Cache for 30 minutes\n\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"" + data.Name + " details retrieved successfully\",\n") methodContent.WriteString(" Data: &data" + data.Name + ",\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateDynamicMethod - Template untuk dynamic method dengan cache func generateDynamicMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Get" + data.Name + "Dynamic godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param fields query string false \"Fields to select (e.g., fields=*.*)\"\n") methodContent.WriteString("// @Param filter[column][operator] query string false \"Dynamic filters (e.g., filter[name][_eq]=value)\"\n") methodContent.WriteString("// @Param sort query string false \"Sort fields (e.g., sort=date_created,-name)\"\n") methodContent.WriteString("// @Param limit query int false \"Limit\" default(10)\n") methodContent.WriteString("// @Param offset query int false \"Offset\" default(0)\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Success response\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "Dynamic(c *gin.Context) {\n") methodContent.WriteString(" parser := queryUtils.NewQueryParser().SetLimits(10, 100)\n") // Get the default fields for this endpoint from the configuration defaultFields := getFieldsForEndpoint(data, endpoint.Fields) methodContent.WriteString(" dynamicQuery, err := parser.ParseQueryWithDefaultFields(c.Request.URL.Query(), \"" + data.TableName + "\", []string{\n") for _, field := range defaultFields { methodContent.WriteString(" \"" + field + "\",\n") } methodContent.WriteString(" })\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Invalid query parameters\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(defaultFields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" dynamicQuery.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(defaultFields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Add default filter to exclude deleted records\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" dynamicQuery.Filters = append([]queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{{Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"}},\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }}, dynamicQuery.Filters...)\n\n") } methodContent.WriteString(" // Try to get from cache first\n") methodContent.WriteString(" // Create cache key from query string\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":dynamic:%s\", c.Request.URL.RawQuery)\n") methodContent.WriteString(" if cachedData, found := h.cache.Get(cacheKey); found {\n") methodContent.WriteString(" logger.Info(\"Cache hit for dynamic query\", map[string]interface{}{\"cache_key\": cacheKey})\n\n") methodContent.WriteString(" // Convert from interface{} to expected type\n") methodContent.WriteString(" if " + data.NamePlural + ", ok := cachedData.([]" + data.Category + "Models." + data.Name + "); ok {\n") methodContent.WriteString(" meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, len(" + data.NamePlural + "))\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"Data " + data.NameLower + " berhasil diambil (dari cache)\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" " + data.NamePlural + ", total, err := h.fetch" + data.Name + "sDynamic(ctx, dbConn, dynamicQuery)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to fetch data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Save to cache\n") methodContent.WriteString(" h.cache.Set(cacheKey, " + data.NamePlural + ", 10*time.Minute) // Cache for 10 minutes\n\n") methodContent.WriteString(" meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total)\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"Data " + data.NameLower + " berhasil diambil\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateSearchMethod - Template untuk search method dengan cache func generateSearchMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Search" + data.Name + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param q query string false \"Search query\"\n") methodContent.WriteString("// @Param limit query int false \"Limit\" default(20)\n") methodContent.WriteString("// @Param offset query int false \"Offset\" default(0)\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Success response\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Search" + data.Name + "(c *gin.Context) {\n") methodContent.WriteString(" // Parse complex search parameters\n") methodContent.WriteString(" searchQuery := c.Query(\"q\")\n") methodContent.WriteString(" if searchQuery == \"\" {\n") methodContent.WriteString(" // If no search query provided, return all records with default sorting\n") // Get the fields for this endpoint from the configuration fields := getFieldsForEndpoint(data, endpoint.Fields) methodContent.WriteString(" query := queryUtils.DynamicQuery{\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") // Generate select fields based on the endpoint configuration for _, field := range fields { methodContent.WriteString(" {Expression: \"" + field + "\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{}, // Empty filters - fetch" + data.Name + "sDynamic will add default deleted filter\n") methodContent.WriteString(" Sort: []queryUtils.SortField{{\n") methodContent.WriteString(" Column: \"date_created\",\n") methodContent.WriteString(" Order: \"DESC\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Limit: 20,\n") methodContent.WriteString(" Offset: 0,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(fields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" query.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(fields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Parse pagination if provided\n") methodContent.WriteString(" if limit := c.Query(\"limit\"); limit != \"\" {\n") methodContent.WriteString(" if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {\n") methodContent.WriteString(" query.Limit = l\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if offset := c.Query(\"offset\"); offset != \"\" {\n") methodContent.WriteString(" if o, err := strconv.Atoi(offset); err == nil && o >= 0 {\n") methodContent.WriteString(" query.Offset = o\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" // Execute query to get all records\n") methodContent.WriteString(" " + data.NamePlural + ", total, err := h.fetch" + data.Name + "sDynamic(ctx, dbConn, query)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to fetch data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" meta := h.calculateMeta(query.Limit, query.Offset, total)\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"All records retrieved (no search query provided)\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build dynamic query for search\n") methodContent.WriteString(" query := queryUtils.DynamicQuery{\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") // Generate select fields based on the endpoint configuration for _, field := range fields { methodContent.WriteString(" {Expression: \"" + field + "\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") // Add searchable columns based on table schema searchableColumns := findSearchableColumns(data.TableSchema) for _, col := range searchableColumns { methodContent.WriteString(" {\n") methodContent.WriteString(" Column: \"" + col + "\",\n") methodContent.WriteString(" Operator: queryUtils.OpContains,\n") methodContent.WriteString(" Value: searchQuery,\n") methodContent.WriteString(" },\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"OR\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Sort: []queryUtils.SortField{{\n") methodContent.WriteString(" Column: \"date_created\",\n") methodContent.WriteString(" Order: \"DESC\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Limit: 20,\n") methodContent.WriteString(" Offset: 0,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(fields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" query.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(fields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Parse pagination if provided\n") methodContent.WriteString(" if limit := c.Query(\"limit\"); limit != \"\" {\n") methodContent.WriteString(" if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {\n") methodContent.WriteString(" query.Limit = l\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if offset := c.Query(\"offset\"); offset != \"\" {\n") methodContent.WriteString(" if o, err := strconv.Atoi(offset); err == nil && o >= 0 {\n") methodContent.WriteString(" query.Offset = o\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Try to get from cache first\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":search:%s:%d:%d\", searchQuery, query.Limit, query.Offset)\n") methodContent.WriteString(" if cachedData, found := h.cache.Get(cacheKey); found {\n") methodContent.WriteString(" logger.Info(\"Cache hit for search\", map[string]interface{}{\"search\": searchQuery, \"cache_key\": cacheKey})\n\n") methodContent.WriteString(" // Convert from interface{} to expected type\n") methodContent.WriteString(" if " + data.NamePlural + ", ok := cachedData.([]" + data.Category + "Models." + data.Name + "); ok {\n") methodContent.WriteString(" meta := h.calculateMeta(query.Limit, query.Offset, len(" + data.NamePlural + "))\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: fmt.Sprintf(\"Search results for '%s' (dari cache)\", searchQuery),\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" // Execute search\n") methodContent.WriteString(" " + data.NamePlural + ", total, err := h.fetch" + data.Name + "sDynamic(ctx, dbConn, query)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Search failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Save to cache\n") methodContent.WriteString(" h.cache.Set(cacheKey, " + data.NamePlural + ", 15*time.Minute) // Cache for 15 minutes\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" meta := h.calculateMeta(query.Limit, query.Offset, total)\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: fmt.Sprintf(\"Search results for '%s'\", searchQuery),\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateStatsMethod - Template untuk stats method dengan cache func generateStatsMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Get" + data.Name + "Stats godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString("// @Param " + statusColumn + " query string false \"Filter statistics by status\"\n") } methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Statistics data\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "Stats(c *gin.Context) {\n") methodContent.WriteString(" // Try to get from cache first\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":stats:%s\", c.Query(\"" + statusColumn + "\"))\n") methodContent.WriteString(" if cachedData, found := h.cache.Get(cacheKey); found {\n") methodContent.WriteString(" logger.Info(\"Cache hit for stats\", map[string]interface{}{\"cache_key\": cacheKey})\n\n") methodContent.WriteString(" // Convert from interface{} to expected type\n") methodContent.WriteString(" if aggregateData, ok := cachedData.(*models.AggregateData); ok {\n") methodContent.WriteString(" c.JSON(http.StatusOK, gin.H{\n") methodContent.WriteString(" \"message\": \"Statistik " + data.NameLower + " berhasil diambil (dari cache)\",\n") methodContent.WriteString(" \"data\": aggregateData,\n") methodContent.WriteString(" })\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" // Build filter groups\n") methodContent.WriteString(" filterGroups := []queryUtils.FilterGroup{{\n") if statusColumn != "" { methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{{Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"}},\n") } methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }}\n\n") if statusColumn != "" { methodContent.WriteString(" // Add status filter if provided\n") methodContent.WriteString(" if " + statusColumn + " := c.Query(\"" + statusColumn + "\"); " + statusColumn + " != \"\" && models.IsValidStatus(" + statusColumn + ") {\n") methodContent.WriteString(" filterGroups = append(filterGroups, queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{{Column: \"" + statusColumn + "\", Operator: queryUtils.OpEqual, Value: " + statusColumn + "}},\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" })\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" aggregateData, err := h.getAggregateData(ctx, dbConn, filterGroups)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get statistics\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Save to cache\n") methodContent.WriteString(" h.cache.Set(cacheKey, aggregateData, 5*time.Minute) // Cache stats for 5 minutes\n\n") methodContent.WriteString(" c.JSON(http.StatusOK, gin.H{\n") methodContent.WriteString(" \"message\": \"Statistik " + data.NameLower + " berhasil diambil\",\n") methodContent.WriteString(" \"data\": aggregateData,\n") methodContent.WriteString(" })\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateByLocationMethod - Template untuk by location method func generateByLocationMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Get" + data.Name + "ByLocation godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") // Find location-related columns locationColumns := findLocationColumns(data.TableSchema) for _, col := range locationColumns { methodContent.WriteString("// @Param " + col + " query int false \"Filter by " + col + " ID\"\n") } methodContent.WriteString("// @Param limit query int false \"Limit (max 100)\" default(10)\n") methodContent.WriteString("// @Param offset query int false \"Offset\" default(0)\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Success response\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "ByLocation(c *gin.Context) {\n") methodContent.WriteString(" // Parse location filters\n") methodContent.WriteString(" var filters []queryUtils.DynamicFilter\n\n") // Add location filters for _, col := range locationColumns { methodContent.WriteString(" if " + col + " := c.Query(\"" + col + "\"); " + col + " != \"\" {\n") methodContent.WriteString(" if " + col + "ID, err := strconv.Atoi(" + col + "); err == nil {\n") methodContent.WriteString(" filters = append(filters, queryUtils.DynamicFilter{Column: \"" + col + "\", Operator: queryUtils.OpEqual, Value: " + col + "ID})\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Parse pagination\n") methodContent.WriteString(" limit, offset := 10, 0\n") methodContent.WriteString(" if limitStr := c.Query(\"limit\"); limitStr != \"\" {\n") methodContent.WriteString(" if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {\n") methodContent.WriteString(" limit = l\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n") methodContent.WriteString(" if offsetStr := c.Query(\"offset\"); offsetStr != \"\" {\n") methodContent.WriteString(" if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {\n") methodContent.WriteString(" offset = o\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Get the fields for this endpoint from the configuration fields := getFieldsForEndpoint(data, endpoint.Fields) methodContent.WriteString(" // Build query\n") methodContent.WriteString(" query := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") // Generate select fields based on the endpoint configuration for _, field := range fields { methodContent.WriteString(" {Expression: \"" + field + "\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Sort: []queryUtils.SortField{{Column: \"date_created\", Order: \"DESC\"}},\n") methodContent.WriteString(" Limit: limit,\n") methodContent.WriteString(" Offset: offset,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist and fields include relationship columns if len(data.Relationships) > 0 && hasRelationshipFields(fields, data.Relationships) { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" query.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { // Check if any field from this relationship is included if hasRelationshipField(fields, rel) { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Add filters if any\n") methodContent.WriteString(" if len(filters) > 0 {\n") methodContent.WriteString(" query.Filters = append(query.Filters, queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: filters,\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" })\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Execute query\n") methodContent.WriteString(" " + data.NamePlural + ", total, err := h.fetch" + data.Name + "sDynamic(ctx, dbConn, query)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to fetch data\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" meta := h.calculateMeta(limit, offset, total)\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: \"Data " + data.NameLower + " by location retrieved successfully\",\n") methodContent.WriteString(" Data: " + data.NamePlural + ",\n") methodContent.WriteString(" Meta: meta,\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateByAgeMethod - Template untuk by age method func generateByAgeMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Get" + data.Name + "ByAge godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param age_group query string false \"Age group (child, teen, adult, senior)\"\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"Statistics data\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [get]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Get" + data.Name + "ByAge(c *gin.Context) {\n") methodContent.WriteString(" // Parse age group\n") methodContent.WriteString(" ageGroup := c.Query(\"age_group\")\n") methodContent.WriteString(" validAgeGroups := map[string]bool{\n") methodContent.WriteString(" \"child\": true, // 0-12 years\n") methodContent.WriteString(" \"teen\": true, // 13-17 years\n") methodContent.WriteString(" \"adult\": true, // 18-59 years\n") methodContent.WriteString(" \"senior\": true, // 60+ years\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if ageGroup == \"\" || !validAgeGroups[ageGroup] {\n") methodContent.WriteString(" h.respondError(c, \"Invalid age group\", fmt.Errorf(\"age group must be one of: child, teen, adult, senior\"), http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Find date of birth column birthDateColumn := findBirthDateColumn(data.TableSchema) // Only declare these variables if we have a birth date column if birthDateColumn != "" { methodContent.WriteString(" // Calculate age range based on group\n") methodContent.WriteString(" var minAge, maxAge int\n") methodContent.WriteString(" now := time.Now()\n") methodContent.WriteString(" switch ageGroup {\n") methodContent.WriteString(" case \"child\":\n") methodContent.WriteString(" maxAge = 12\n") methodContent.WriteString(" case \"teen\":\n") methodContent.WriteString(" minAge = 13\n") methodContent.WriteString(" maxAge = 17\n") methodContent.WriteString(" case \"adult\":\n") methodContent.WriteString(" minAge = 18\n") methodContent.WriteString(" maxAge = 59\n") methodContent.WriteString(" case \"senior\":\n") methodContent.WriteString(" minAge = 60\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Build query\n") methodContent.WriteString(" query := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{\n") methodContent.WriteString(" {Expression: \"COUNT(*)\", Alias: \"count\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{{\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"},\n") methodContent.WriteString(" },\n") } methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" }\n\n") if birthDateColumn != "" { methodContent.WriteString(" // Add age filter if applicable\n") methodContent.WriteString(" if minAge > 0 {\n") methodContent.WriteString(" minBirthDate := now.AddDate(-maxAge-1, 0, 0)\n") methodContent.WriteString(" query.Filters[0].Filters = append(query.Filters[0].Filters, \n") methodContent.WriteString(" queryUtils.DynamicFilter{Column: \"" + birthDateColumn + "\", Operator: queryUtils.OpGreaterThanEqual, Value: minBirthDate})\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if maxAge > 0 {\n") methodContent.WriteString(" maxBirthDate := now.AddDate(-minAge, 0, 0)\n") methodContent.WriteString(" query.Filters[0].Filters = append(query.Filters[0].Filters, \n") methodContent.WriteString(" queryUtils.DynamicFilter{Column: \"" + birthDateColumn + "\", Operator: queryUtils.OpLessThan, Value: maxBirthDate})\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" // Execute query\n") methodContent.WriteString(" var result struct {\n") methodContent.WriteString(" Count int `db:\"count\"`\n") methodContent.WriteString(" }\n") methodContent.WriteString(" err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, query, &result)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get age statistics\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Build response\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{\n") methodContent.WriteString(" Message: fmt.Sprintf(\"Age group '%s' statistics retrieved successfully\", ageGroup),\n") methodContent.WriteString(" Data: map[string]interface{}{\n") methodContent.WriteString(" \"age_group\": ageGroup,\n") methodContent.WriteString(" \"count\": result.Count,\n") methodContent.WriteString(" },\n") methodContent.WriteString(" }\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateCreateMethod - Template untuk create method dengan validation func generateCreateMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder methodContent.WriteString("// Create" + data.Name + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param request body " + data.Category + "Models." + endpoint.RequestModel + " true \"" + data.Name + " creation request\"\n") methodContent.WriteString("// @Success 201 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"" + data.Name + " created successfully\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request or validation error\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [post]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Create" + data.Name + "(c *gin.Context) {\n") methodContent.WriteString(" var req " + data.Category + "Models." + endpoint.RequestModel + "\n") methodContent.WriteString(" if err := c.ShouldBindJSON(&req); err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Invalid request body\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" if err := validate.Struct(&req); err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Validation failed\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Add validation for unique fields if they exist uniqueColumns := findUniqueColumns(data.TableSchema) for _, col := range uniqueColumns { methodContent.WriteString(" // Validate " + col + " must be unique\n") methodContent.WriteString(" if req." + snakeToPascal(col) + " != nil {\n") methodContent.WriteString(" rule := validation.NewUniqueFieldRule(\n") methodContent.WriteString(" \"" + data.TableName + "\", // Table name\n") methodContent.WriteString(" \"" + col + "\", // Column that must be unique\n") methodContent.WriteString(" queryUtils.DynamicFilter{ // Additional condition\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" Column: \"" + statusColumn + "\",\n") methodContent.WriteString(" Operator: queryUtils.OpNotEqual,\n") methodContent.WriteString(" Value: \"deleted\",\n") } else { methodContent.WriteString(" Column: \"id\",\n") methodContent.WriteString(" Operator: queryUtils.OpNotEqual,\n") methodContent.WriteString(" Value: \"0\",\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" )\n\n") methodContent.WriteString(" // Prepare data from request for validation\n") methodContent.WriteString(" dataToValidate := map[string]interface{}{\n") methodContent.WriteString(" \"" + col + "\": *req." + snakeToPascal(col) + ",\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Execute validation\n") methodContent.WriteString(" isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to validate " + col + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if isDuplicate {\n") methodContent.WriteString(" h.respondError(c, \"" + col + " already exists\", fmt.Errorf(\"duplicate " + col + ": %d\", *req." + snakeToPascal(col) + "), http.StatusConflict)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" data := queryUtils.InsertData{\n") methodContent.WriteString(" Columns: []string{\n") // Add primary key if it's not auto-generated primaryKey := findPrimaryKey(data.TableSchema) if !isAutoGeneratedPrimaryKey(data.TableSchema) { methodContent.WriteString(" \"" + primaryKey + "\",\n") } // Add status column if it exists statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { methodContent.WriteString(" \"" + statusColumn + "\",\n") } methodContent.WriteString(" \"date_created\", \"date_updated\",\n") // Add all non-system columns systemFields := findSystemFields(data.TableSchema) for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" \"" + col.Name + "\",\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Values: []interface{}{\n") // Add primary key value if it's not auto-generated if !isAutoGeneratedPrimaryKey(data.TableSchema) { methodContent.WriteString(" req." + snakeToPascal(primaryKey) + ",\n") } // Add status value if it exists if statusColumn != "" { methodContent.WriteString(" req." + snakeToPascal(statusColumn) + ",\n") } methodContent.WriteString(" time.Now(), time.Now(),\n") // Add all non-system column values for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" req." + snakeToPascal(col.Name) + ",\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" }\n") methodContent.WriteString(" returningCols := []string{\n") // Add primary key if it's not auto-generated if !isAutoGeneratedPrimaryKey(data.TableSchema) { methodContent.WriteString(" \"" + primaryKey + "\",\n") } // Add status column if it exists if statusColumn != "" { methodContent.WriteString(" \"" + statusColumn + "\",\n") } methodContent.WriteString(" \"sort\", \"user_created\", \"date_created\", \"user_updated\", \"date_updated\",\n") // Add all non-system columns for returning for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" \"" + col.Name + "\",\n") } methodContent.WriteString(" }\n\n") methodContent.WriteString(" sql, args, err := h.queryBuilder.BuildInsertQuery(\"" + data.TableName + "\", data, returningCols...)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to build insert query\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" var data" + data.Name + " " + data.Category + "Models." + data.Name + "\n") methodContent.WriteString(" err = dbConn.GetContext(ctx, &data" + data.Name + ", sql, args...)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to create " + data.NameLower + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Invalidate cache that might be affected\n") methodContent.WriteString(" h.invalidateRelatedCache()\n\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{Message: \"" + data.Name + " berhasil dibuat\", Data: &data" + data.Name + "}\n") methodContent.WriteString(" c.JSON(http.StatusCreated, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateUpdateMethod - Template untuk update method dengan cache yang diperbaiki func generateUpdateMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" // Default fallback } // Define statusColumn di awal fungsi statusColumn := findStatusColumn(data.TableSchema) methodContent.WriteString("// Update" + data.Name + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param " + primaryKey + " path string true \"" + data.Name + " " + strings.ToUpper(primaryKey) + "\"\n") methodContent.WriteString("// @Param request body " + data.Category + "Models." + endpoint.RequestModel + " true \"" + data.Name + " update request\"\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"" + data.Name + " updated successfully\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Bad request or validation error\"\n") methodContent.WriteString("// @Failure 404 {object} models.ErrorResponse \"" + data.Name + " not found\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [put]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Update" + data.Name + "(c *gin.Context) {\n") methodContent.WriteString(" " + primaryKey + " := c.Param(\"" + primaryKey + "\")\n") methodContent.WriteString(" if " + primaryKey + " == \"\" {\n") methodContent.WriteString(" h.respondError(c, \"Invalid " + strings.ToUpper(primaryKey) + " format\", fmt.Errorf(\"" + primaryKey + " cannot be empty\"), http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" var req " + data.Category + "Models." + endpoint.RequestModel + "\n") methodContent.WriteString(" if err := c.ShouldBindJSON(&req); err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Invalid request body\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") // Get the type of the primary key _, pkBaseType, _ := mapSQLTypeToGo(getColumnType(data.TableSchema, primaryKey), isColumnNullable(data.TableSchema, primaryKey), "") // Generate ID conversion based on primary key type if pkBaseType == "int32" { methodContent.WriteString(" idInt, err := strconv.Atoi(" + primaryKey + ")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Invalid ID format\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" idInt32 := int32(idInt)\n") methodContent.WriteString(" req." + snakeToPascal(primaryKey) + " = &idInt32\n") } else if pkBaseType == "int64" { methodContent.WriteString(" idInt, err := strconv.ParseInt(" + primaryKey + ", 10, 64)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Invalid ID format\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" req." + snakeToPascal(primaryKey) + " = &idInt\n") } else if pkBaseType == "string" { methodContent.WriteString(" req." + snakeToPascal(primaryKey) + " = &" + primaryKey + "\n") } methodContent.WriteString(" if err := validate.Struct(&req); err != nil {\n") methodContent.WriteString(" h.respondError(c, \"Validation failed\", err, http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Try to get old data for cache invalidation\n") methodContent.WriteString(" var oldData " + data.Category + "Models." + data.Name + "\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err == nil {\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" dynamicQuery := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{{Expression: \"*\"}},\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: " + primaryKey + "},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Limit: 1,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist if len(data.Relationships) > 0 { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" dynamicQuery.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &oldData)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" logger.Error(\"Failed to fetch old data for cache invalidation\", map[string]interface{}{\"error\": err.Error(), \"" + primaryKey + "\": " + primaryKey + "})\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err = h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") // Add validation for unique fields if they exist uniqueColumns := findUniqueColumns(data.TableSchema) for _, col := range uniqueColumns { methodContent.WriteString(" // Validate " + col + " must be unique, except for record with this " + primaryKey + "\n") methodContent.WriteString(" if req." + snakeToPascal(col) + " != nil {\n") methodContent.WriteString(" rule := validation.ValidationRule{\n") methodContent.WriteString(" TableName: \"" + data.TableName + "\",\n") methodContent.WriteString(" UniqueColumns: []string{\"" + col + "\"},\n") methodContent.WriteString(" Conditions: []queryUtils.DynamicFilter{\n") if statusColumn != "" { methodContent.WriteString(" {Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" ExcludeIDColumn: \"" + primaryKey + "\", // Exclude based on '" + primaryKey + "' column\n") methodContent.WriteString(" ExcludeIDValue: " + primaryKey + ", // ...with " + primaryKey + " value from parameter\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" dataToValidate := map[string]interface{}{\n") methodContent.WriteString(" \"" + col + "\": *req." + snakeToPascal(col) + ",\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to validate " + col + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" if isDuplicate {\n") methodContent.WriteString(" h.respondError(c, \"" + col + " already exists\", fmt.Errorf(\"duplicate " + col + ": %d\", *req." + snakeToPascal(col) + "), http.StatusConflict)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") } methodContent.WriteString(" updateData := queryUtils.UpdateData{\n") methodContent.WriteString(" Columns: []string{\n") // Add status column if it exists if statusColumn != "" { methodContent.WriteString(" \"" + statusColumn + "\",\n") } methodContent.WriteString(" \"date_updated\",\n") // Add all non-system columns systemFields := findSystemFields(data.TableSchema) for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" \"" + col.Name + "\",\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" Values: []interface{}{\n") // Add status value if it exists if statusColumn != "" { methodContent.WriteString(" req." + snakeToPascal(statusColumn) + ",\n") } methodContent.WriteString(" time.Now(),\n") // Add all non-system column values for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" req." + snakeToPascal(col.Name) + ",\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" }\n") methodContent.WriteString(" filters := []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: req." + snakeToPascal(primaryKey) + "},\n") if statusColumn != "" { methodContent.WriteString(" {Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"},\n") } methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }}\n") methodContent.WriteString(" returningCols := []string{\n") // Add primary key if it's not auto-generated if !isAutoGeneratedPrimaryKey(data.TableSchema) { methodContent.WriteString(" \"" + primaryKey + "\",\n") } // Add status column if it exists if statusColumn != "" { methodContent.WriteString(" \"" + statusColumn + "\",\n") } methodContent.WriteString(" \"sort\", \"user_created\", \"date_created\", \"user_updated\", \"date_updated\",\n") // Add all non-system columns for returning for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } methodContent.WriteString(" \"" + col.Name + "\",\n") } methodContent.WriteString(" }\n\n") methodContent.WriteString(" sql, args, err := h.queryBuilder.BuildUpdateQuery(\"" + data.TableName + "\", updateData, filters, returningCols...)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to build update query\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" var data" + data.Name + " " + data.Category + "Models." + data.Name + "\n") methodContent.WriteString(" err = dbConn.GetContext(ctx, &data" + data.Name + ", sql, args...)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" if err.Error() == \"sql: no rows in result set\" {\n") methodContent.WriteString(" h.respondError(c, \"" + data.Name + " not found\", err, http.StatusNotFound)\n") methodContent.WriteString(" } else {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to update " + data.NameLower + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" }\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Invalidate cache that might be affected\n") methodContent.WriteString(" // Invalidate cache for " + primaryKey + " that was updated\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":" + primaryKey + ":%s\", " + primaryKey + ")\n") methodContent.WriteString(" h.cache.Delete(cacheKey)\n\n") methodContent.WriteString(" // Invalidate cache for old and new data\n") methodContent.WriteString(" if oldData." + snakeToPascal(primaryKey) + " != 0 {\n") methodContent.WriteString(" h.invalidateRelatedCache()\n") methodContent.WriteString(" }\n") methodContent.WriteString(" h.invalidateRelatedCache()\n\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{Message: \"" + data.Name + " berhasil diperbarui\", Data: &data" + data.Name + "}\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateDeleteMethod - Template untuk delete method dengan cache func generateDeleteMethod(data HandlerData, endpoint EndpointConfig) string { var methodContent strings.Builder primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" // Default fallback } methodContent.WriteString("// Delete" + data.Name + " godoc\n") methodContent.WriteString("// @Summary " + endpoint.Summary + "\n") methodContent.WriteString("// @Description " + endpoint.Description + "\n") methodContent.WriteString("// @Tags " + strings.Join(endpoint.Tags, ", ") + "\n") methodContent.WriteString("// @Accept json\n") methodContent.WriteString("// @Produce json\n") methodContent.WriteString("// @Param " + primaryKey + " path string true \"" + data.Name + " " + strings.ToUpper(primaryKey) + "\"\n") methodContent.WriteString("// @Success 200 {object} " + data.Category + "Models." + endpoint.ResponseModel + " \"" + data.Name + " deleted successfully\"\n") methodContent.WriteString("// @Failure 400 {object} models.ErrorResponse \"Invalid ID format\"\n") methodContent.WriteString("// @Failure 404 {object} models.ErrorResponse \"" + data.Name + " not found\"\n") methodContent.WriteString("// @Failure 500 {object} models.ErrorResponse \"Internal server error\"\n") methodContent.WriteString("// @Router /api/v1/" + strings.ToLower(data.Name) + endpoint.Path + " [delete]\n") methodContent.WriteString("func (h *" + data.Name + "Handler) Delete" + data.Name + "(c *gin.Context) {\n") methodContent.WriteString(" " + primaryKey + " := c.Param(\"" + primaryKey + "\")\n") methodContent.WriteString(" if " + primaryKey + " == \"\" {\n") methodContent.WriteString(" h.respondError(c, \"Invalid " + strings.ToUpper(primaryKey) + " format\", fmt.Errorf(\"" + primaryKey + " cannot be empty\"), http.StatusBadRequest)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Try to get data for cache invalidation\n") methodContent.WriteString(" var dataToDelete " + data.Category + "Models." + data.Name + "\n") methodContent.WriteString(" dbConn, err := h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err == nil {\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") methodContent.WriteString(" dynamicQuery := queryUtils.DynamicQuery{\n") methodContent.WriteString(" From: \"" + data.TableName + "\",\n") methodContent.WriteString(" Fields: []queryUtils.SelectField{{Expression: \"*\"}},\n") methodContent.WriteString(" Filters: []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: " + primaryKey + "},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }},\n") methodContent.WriteString(" Limit: 1,\n") methodContent.WriteString(" }\n\n") // Add joins if relationships exist if len(data.Relationships) > 0 { methodContent.WriteString(" // Add joins for relationships using the correct structure\n") methodContent.WriteString(" dynamicQuery.Joins = []queryUtils.Join{\n") for _, rel := range data.Relationships { methodContent.WriteString(" {\n") methodContent.WriteString(" Type: \"LEFT\",\n") methodContent.WriteString(" Table: \"" + rel.Table + "\",\n") methodContent.WriteString(" Alias: \"" + rel.Table + "\",\n") methodContent.WriteString(" OnConditions: queryUtils.FilterGroup{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + data.TableName + "." + rel.ForeignKey + "\", Operator: queryUtils.OpEqual, Value: \"" + rel.Table + "." + rel.LocalKey + "\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") methodContent.WriteString(" },\n") } methodContent.WriteString(" }\n\n") } methodContent.WriteString(" err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &dataToDelete)\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" logger.Error(\"Failed to fetch data for cache invalidation\", map[string]interface{}{\"error\": err.Error(), \"" + primaryKey + "\": " + primaryKey + "})\n") methodContent.WriteString(" }\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Use GetSQLXDB to get database connection\n") methodContent.WriteString(" dbConn, err = h.db.GetSQLXDB(\"postgres_satudata\")\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Database connection failed\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)\n") methodContent.WriteString(" defer cancel()\n\n") if endpoint.SoftDelete { statusColumn := findStatusColumn(data.TableSchema) methodContent.WriteString(" // Use ExecuteUpdate for soft delete by changing status\n") methodContent.WriteString(" updateData := queryUtils.UpdateData{\n") methodContent.WriteString(" Columns: []string{\"" + statusColumn + "\", \"date_updated\"},\n") methodContent.WriteString(" Values: []interface{}{\"deleted\", time.Now()},\n") methodContent.WriteString(" }\n") methodContent.WriteString(" filters := []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: " + primaryKey + "},\n") methodContent.WriteString(" {Column: \"" + statusColumn + "\", Operator: queryUtils.OpNotEqual, Value: \"deleted\"},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }}\n\n") methodContent.WriteString(" // Use ExecuteUpdate instead of ExecuteDelete\n") methodContent.WriteString(" result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, \"" + data.TableName + "\", updateData, filters)\n") } else { methodContent.WriteString(" // Use ExecuteDelete for hard delete\n") methodContent.WriteString(" filters := []queryUtils.FilterGroup{{\n") methodContent.WriteString(" Filters: []queryUtils.DynamicFilter{\n") methodContent.WriteString(" {Column: \"" + primaryKey + "\", Operator: queryUtils.OpEqual, Value: " + primaryKey + "},\n") methodContent.WriteString(" },\n") methodContent.WriteString(" LogicOp: \"AND\",\n") methodContent.WriteString(" }}\n\n") methodContent.WriteString(" result, err := h.queryBuilder.ExecuteDelete(ctx, dbConn, \"" + data.TableName + "\", filters)\n") } methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to delete " + data.NameLower + "\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" rowsAffected, err := result.RowsAffected()\n") methodContent.WriteString(" if err != nil {\n") methodContent.WriteString(" h.logAndRespondError(c, \"Failed to get affected rows\", err, http.StatusInternalServerError)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n") methodContent.WriteString(" if rowsAffected == 0 {\n") methodContent.WriteString(" h.respondError(c, \"" + data.Name + " not found\", sql.ErrNoRows, http.StatusNotFound)\n") methodContent.WriteString(" return\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" // Invalidate cache that might be affected\n") methodContent.WriteString(" // Invalidate cache for " + primaryKey + " that was deleted\n") methodContent.WriteString(" cacheKey := fmt.Sprintf(\"" + data.NameLower + ":" + primaryKey + ":%s\", " + primaryKey + ")\n") methodContent.WriteString(" h.cache.Delete(cacheKey)\n\n") methodContent.WriteString(" // Invalidate cache for data that was deleted\n") methodContent.WriteString(" if dataToDelete." + snakeToPascal(primaryKey) + " != 0 {\n") methodContent.WriteString(" h.invalidateRelatedCache()\n") methodContent.WriteString(" }\n\n") methodContent.WriteString(" response := " + data.Category + "Models." + endpoint.ResponseModel + "{Message: \"" + data.Name + " berhasil dihapus\", " + snakeToPascal(primaryKey) + ": " + primaryKey + "}\n") methodContent.WriteString(" c.JSON(http.StatusOK, response)\n") methodContent.WriteString("}\n\n") return methodContent.String() } // generateHelperMethodsWithCache - Template untuk helper methods dengan cache func generateHelperMethodsWithCache(data HandlerData) string { var helperMethods strings.Builder primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" // Default fallback } // Invalidate cache method helperMethods.WriteString("// invalidateRelatedCache invalidates cache that might be affected by data changes\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) invalidateRelatedCache() {\n") helperMethods.WriteString(" // Invalidate cache for search that might be affected\n") helperMethods.WriteString(" h.cache.DeleteByPrefix(\"" + data.NameLower + ":search:\")\n") helperMethods.WriteString(" h.cache.DeleteByPrefix(\"" + data.NameLower + ":dynamic:\")\n") helperMethods.WriteString(" h.cache.DeleteByPrefix(\"" + data.NameLower + ":stats:\")\n") helperMethods.WriteString(" h.cache.DeleteByPrefix(\"" + data.NameLower + ":" + primaryKey + ":\")\n") helperMethods.WriteString("}\n\n") // Fetch dynamic method helperMethods.WriteString("// fetch" + data.Name + "sDynamic executes dynamic query with timeout handling\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) fetch" + data.Name + "sDynamic(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]" + data.Category + "Models." + data.Name + ", int, error) {\n") helperMethods.WriteString(" logger.Info(\"Starting fetch" + data.Name + "sDynamic\", map[string]interface{}{\n") helperMethods.WriteString(" \"limit\": query.Limit,\n") helperMethods.WriteString(" \"offset\": query.Offset,\n") helperMethods.WriteString(" \"from\": query.From,\n") helperMethods.WriteString(" })\n\n") helperMethods.WriteString(" var total int\n") helperMethods.WriteString(" var " + data.NamePlural + " []" + data.Category + "Models." + data.Name + "\n\n") helperMethods.WriteString(" // Check if query has search\n") helperMethods.WriteString(" hasSearch := false\n") helperMethods.WriteString(" for _, filterGroup := range query.Filters {\n") helperMethods.WriteString(" for _, filter := range filterGroup.Filters {\n") helperMethods.WriteString(" if filter.Operator == queryUtils.OpILike {\n") helperMethods.WriteString(" hasSearch = true\n") helperMethods.WriteString(" break\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" if hasSearch {\n") helperMethods.WriteString(" break\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" logger.Info(\"Query analysis\", map[string]interface{}{\n") helperMethods.WriteString(" \"hasSearch\": hasSearch,\n") helperMethods.WriteString(" \"totalFilters\": len(query.Filters),\n") helperMethods.WriteString(" })\n\n") helperMethods.WriteString(" // Optimize to prevent timeout on search queries\n") helperMethods.WriteString(" // Use shorter context for search and count queries\n") helperMethods.WriteString(" queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)\n") helperMethods.WriteString(" defer queryCancel()\n\n") helperMethods.WriteString(" // For search queries, limit maximum to prevent timeout\n") helperMethods.WriteString(" if hasSearch {\n") helperMethods.WriteString(" search := getSearchTerm(query)\n") helperMethods.WriteString(" logger.Info(\"Executing search query with timeout context\", map[string]interface{}{\"search_term\": search})\n\n") helperMethods.WriteString(" // Limit maximum search limit to prevent timeout\n") helperMethods.WriteString(" maxSearchLimit := 50\n") helperMethods.WriteString(" if query.Limit > maxSearchLimit {\n") helperMethods.WriteString(" query.Limit = maxSearchLimit\n") helperMethods.WriteString(" logger.Info(\"Reduced search limit to prevent timeout\", map[string]interface{}{\n") helperMethods.WriteString(" \"original_limit\": query.Limit,\n") helperMethods.WriteString(" \"new_limit\": maxSearchLimit,\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" // Execute search query\n") helperMethods.WriteString(" err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &" + data.NamePlural + ")\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" // Check if it's a PostgreSQL statement timeout error\n") helperMethods.WriteString(" if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == \"57014\" {\n") helperMethods.WriteString(" logger.Warn(\"Search query timed out, trying fallback strategy\", map[string]interface{}{\n") helperMethods.WriteString(" \"search_term\": search,\n") helperMethods.WriteString(" })\n\n") helperMethods.WriteString(" // Fallback: Search only in the most relevant column\n") helperMethods.WriteString(" // We need to rebuild the filters for the fallback\n") helperMethods.WriteString(" var fallbackFilters []queryUtils.FilterGroup\n") helperMethods.WriteString(" // Add other non-search filters back (e.g., status)\n") helperMethods.WriteString(" for _, fg := range query.Filters {\n") helperMethods.WriteString(" if fg.LogicOp == \"AND\" {\n") helperMethods.WriteString(" fallbackFilters = append(fallbackFilters, fg)\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" // Add the single, more specific search filter\n") // Perbaikan: Menggunakan data.TableSchema langsung alih-alih h.tableSchema helperMethods.WriteString(" searchableColumns := []string{\n") // Generate searchable columns from table schema searchableColumns := findSearchableColumns(data.TableSchema) for i, col := range searchableColumns { if i < len(searchableColumns)-1 { helperMethods.WriteString(" \"" + col + "\",\n") } else { helperMethods.WriteString(" \"" + col + "\",\n") } } helperMethods.WriteString(" }\n") helperMethods.WriteString(" if len(searchableColumns) > 0 {\n") helperMethods.WriteString(" fallbackFilters = append([]queryUtils.FilterGroup{{\n") helperMethods.WriteString(" Filters: []queryUtils.DynamicFilter{\n") helperMethods.WriteString(" {Column: searchableColumns[0], Operator: queryUtils.OpILike, Value: \"%\" + search + \"%\"},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" LogicOp: \"AND\",\n") helperMethods.WriteString(" }}, fallbackFilters...)\n\n") helperMethods.WriteString(" fallbackQuery := query\n") helperMethods.WriteString(" fallbackQuery.Filters = fallbackFilters\n\n") helperMethods.WriteString(" // Execute the fallback query with a shorter timeout\n") helperMethods.WriteString(" fallbackCtx, fallbackCancel := context.WithTimeout(ctx, 10*time.Second)\n") helperMethods.WriteString(" defer fallbackCancel()\n\n") helperMethods.WriteString(" err = h.queryBuilder.ExecuteQuery(fallbackCtx, dbConn, fallbackQuery, &" + data.NamePlural + ")\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" logger.Error(\"Fallback search query also failed\", map[string]interface{}{\n") helperMethods.WriteString(" \"error\": err.Error(),\n") helperMethods.WriteString(" \"query\": fallbackQuery,\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" // Return a more user-friendly error\n") helperMethods.WriteString(" return nil, 0, fmt.Errorf(\"search timed out. The search term '%s' is too general. Please try a more specific term\", search)\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" logger.Info(\"Fallback search query successful\", map[string]interface{}{\n") helperMethods.WriteString(" \"recordsFetched\": len(" + data.NamePlural + "),\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" } else {\n") helperMethods.WriteString(" // It's a different error, handle it as before\n") helperMethods.WriteString(" logger.Error(\"Failed to execute search query\", map[string]interface{}{\n") helperMethods.WriteString(" \"error\": err.Error(),\n") helperMethods.WriteString(" \"query\": query,\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" return nil, 0, fmt.Errorf(\"failed to execute search query: %w\", err)\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" // Estimate total for search query (don't count exact for performance)\n") helperMethods.WriteString(" total = len(" + data.NamePlural + ")\n") helperMethods.WriteString(" if len(" + data.NamePlural + ") == query.Limit {\n") helperMethods.WriteString(" // If reached limit, estimate there are more data\n") helperMethods.WriteString(" total = query.Offset + query.Limit + 100\n") helperMethods.WriteString(" } else {\n") helperMethods.WriteString(" total = query.Offset + len(" + data.NamePlural + ")\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" } else {\n") helperMethods.WriteString(" logger.Info(\"Executing regular query without search\")\n\n") helperMethods.WriteString(" // For queries without search, count total with shorter timeout\n") helperMethods.WriteString(" countCtx, countCancel := context.WithTimeout(ctx, 15*time.Second)\n") helperMethods.WriteString(" defer countCancel()\n\n") helperMethods.WriteString(" count, err := h.queryBuilder.ExecuteCount(countCtx, dbConn, query)\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" // If count failed, fallback to estimation or return error\n") helperMethods.WriteString(" logger.Warn(\"Failed to get exact count, using estimation\", map[string]interface{}{\"error\": err.Error()})\n") helperMethods.WriteString(" // For queries without search, we can estimate based on limit\n") helperMethods.WriteString(" total = query.Offset + query.Limit + 100 // Conservative estimation\n") helperMethods.WriteString(" } else {\n") helperMethods.WriteString(" total = int(count)\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" logger.Info(\"Count query successful\", map[string]interface{}{\n") helperMethods.WriteString(" \"count\": total,\n") helperMethods.WriteString(" })\n\n") helperMethods.WriteString(" // Execute main data query\n") helperMethods.WriteString(" err = h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &" + data.NamePlural + ")\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" logger.Error(\"Failed to execute main query\", map[string]interface{}{\n") helperMethods.WriteString(" \"error\": err.Error(),\n") helperMethods.WriteString(" \"query\": query,\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" return nil, 0, fmt.Errorf(\"failed to execute main query: %w\", err)\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" logger.Info(\"Data query successful\", map[string]interface{}{\n") helperMethods.WriteString(" \"recordsFetched\": len(" + data.NamePlural + "),\n") helperMethods.WriteString(" })\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" logger.Info(\"Query execution completed\", map[string]interface{}{\n") helperMethods.WriteString(" \"totalRecords\": total,\n") helperMethods.WriteString(" \"returnedRecords\": len(" + data.NamePlural + "),\n") helperMethods.WriteString(" \"hasSearch\": hasSearch,\n") helperMethods.WriteString(" })\n\n") helperMethods.WriteString(" return " + data.NamePlural + ", total, nil\n") helperMethods.WriteString("}\n\n") // getSearchTerm helper helperMethods.WriteString("// getSearchTerm extracts the search term from a DynamicQuery object.\n") helperMethods.WriteString("// It assumes the search is the first filter group with an \"OR\" logic operator.\n") helperMethods.WriteString("func getSearchTerm(query queryUtils.DynamicQuery) string {\n") helperMethods.WriteString(" for _, filterGroup := range query.Filters {\n") helperMethods.WriteString(" if filterGroup.LogicOp == \"OR\" && len(filterGroup.Filters) > 0 {\n") helperMethods.WriteString(" if valueStr, ok := filterGroup.Filters[0].Value.(string); ok {\n") helperMethods.WriteString(" return strings.Trim(valueStr, \"%\")\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" return \"\"\n") helperMethods.WriteString("}\n\n") // getAggregateData helper helperMethods.WriteString("// getAggregateData gets comprehensive statistics about " + data.NameLower + " data\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) getAggregateData(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) {\n") helperMethods.WriteString(" aggregate := &models.AggregateData{\n") helperMethods.WriteString(" ByStatus: make(map[string]int),\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" var wg sync.WaitGroup\n") helperMethods.WriteString(" var mu sync.Mutex\n") helperMethods.WriteString(" errChan := make(chan error, 4)\n\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { // Count by status helperMethods.WriteString(" // 1. Count by status\n") helperMethods.WriteString(" wg.Add(1)\n") helperMethods.WriteString(" go func() {\n") helperMethods.WriteString(" defer wg.Done()\n") helperMethods.WriteString(" // Use context with shorter timeout\n") helperMethods.WriteString(" queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second)\n") helperMethods.WriteString(" defer queryCancel()\n\n") helperMethods.WriteString(" query := queryUtils.DynamicQuery{\n") helperMethods.WriteString(" From: \"" + data.TableName + "\",\n") helperMethods.WriteString(" Fields: []queryUtils.SelectField{\n") helperMethods.WriteString(" {Expression: \"" + statusColumn + "\"},\n") helperMethods.WriteString(" {Expression: \"COUNT(*)\", Alias: \"count\"},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" Filters: filterGroups,\n") helperMethods.WriteString(" GroupBy: []string{\"" + statusColumn + "\"},\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" var results []struct {\n") helperMethods.WriteString(" Status string `db:\"" + statusColumn + "\"`\n") helperMethods.WriteString(" Count int `db:\"count\"`\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results)\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" errChan <- fmt.Errorf(\"status query failed: %w\", err)\n") helperMethods.WriteString(" return\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" mu.Lock()\n") helperMethods.WriteString(" for _, result := range results {\n") helperMethods.WriteString(" aggregate.ByStatus[result.Status] = result.Count\n") helperMethods.WriteString(" switch result.Status {\n") helperMethods.WriteString(" case \"active\":\n") helperMethods.WriteString(" aggregate.TotalActive = result.Count\n") helperMethods.WriteString(" case \"draft\":\n") helperMethods.WriteString(" aggregate.TotalDraft = result.Count\n") helperMethods.WriteString(" case \"inactive\":\n") helperMethods.WriteString(" aggregate.TotalInactive = result.Count\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" mu.Unlock()\n") helperMethods.WriteString(" }()\n\n") } // Get last updated and today's stats helperMethods.WriteString(" // 4. Get last updated and today's stats\n") helperMethods.WriteString(" wg.Add(1)\n") helperMethods.WriteString(" go func() {\n") helperMethods.WriteString(" defer wg.Done()\n") helperMethods.WriteString(" // Use context with shorter timeout\n") helperMethods.WriteString(" queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second)\n") helperMethods.WriteString(" defer queryCancel()\n\n") helperMethods.WriteString(" // Last updated\n") helperMethods.WriteString(" query1 := queryUtils.DynamicQuery{\n") helperMethods.WriteString(" From: \"" + data.TableName + "\",\n") helperMethods.WriteString(" Fields: []queryUtils.SelectField{{Expression: \"MAX(date_updated)\"}},\n") helperMethods.WriteString(" Filters: filterGroups,\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" var lastUpdated sql.NullTime\n") helperMethods.WriteString(" err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated)\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" errChan <- fmt.Errorf(\"last updated query failed: %w\", err)\n") helperMethods.WriteString(" return\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" // Using QueryBuilder for today's statistics\n") helperMethods.WriteString(" today := time.Now().Format(\"2006-01-02\")\n\n") helperMethods.WriteString(" // Query for created_today\n") helperMethods.WriteString(" createdTodayQuery := queryUtils.DynamicQuery{\n") helperMethods.WriteString(" From: \"" + data.TableName + "\",\n") helperMethods.WriteString(" Fields: []queryUtils.SelectField{\n") helperMethods.WriteString(" {Expression: \"COUNT(*)\", Alias: \"count\"},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" Filters: append(filterGroups, queryUtils.FilterGroup{\n") helperMethods.WriteString(" Filters: []queryUtils.DynamicFilter{\n") helperMethods.WriteString(" {Column: \"DATE(date_created)\", Operator: queryUtils.OpEqual, Value: today},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" LogicOp: \"AND\",\n") helperMethods.WriteString(" }),\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" var createdToday int\n") helperMethods.WriteString(" err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday)\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" errChan <- fmt.Errorf(\"created today query failed: %w\", err)\n") helperMethods.WriteString(" return\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" // Query for updated_today (updated today but not created today)\n") helperMethods.WriteString(" updatedTodayQuery := queryUtils.DynamicQuery{\n") helperMethods.WriteString(" From: \"" + data.TableName + "\",\n") helperMethods.WriteString(" Fields: []queryUtils.SelectField{\n") helperMethods.WriteString(" {Expression: \"COUNT(*)\", Alias: \"count\"},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" Filters: append(filterGroups, queryUtils.FilterGroup{\n") helperMethods.WriteString(" Filters: []queryUtils.DynamicFilter{\n") helperMethods.WriteString(" {Column: \"DATE(date_updated)\", Operator: queryUtils.OpEqual, Value: today},\n") helperMethods.WriteString(" {Column: \"DATE(date_created)\", Operator: queryUtils.OpNotEqual, Value: today},\n") helperMethods.WriteString(" },\n") helperMethods.WriteString(" LogicOp: \"AND\",\n") helperMethods.WriteString(" }),\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" var updatedToday int\n") helperMethods.WriteString(" err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday)\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" errChan <- fmt.Errorf(\"updated today query failed: %w\", err)\n") helperMethods.WriteString(" return\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" mu.Lock()\n") helperMethods.WriteString(" if lastUpdated.Valid {\n") helperMethods.WriteString(" aggregate.LastUpdated = &lastUpdated.Time\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" aggregate.CreatedToday = createdToday\n") helperMethods.WriteString(" aggregate.UpdatedToday = updatedToday\n") helperMethods.WriteString(" mu.Unlock()\n") helperMethods.WriteString(" }()\n\n") helperMethods.WriteString(" wg.Wait()\n") helperMethods.WriteString(" close(errChan)\n\n") helperMethods.WriteString(" for err := range errChan {\n") helperMethods.WriteString(" if err != nil {\n") helperMethods.WriteString(" return nil, err\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" }\n\n") helperMethods.WriteString(" return aggregate, nil\n") helperMethods.WriteString("}\n\n") // Error handling methods helperMethods.WriteString("// logAndRespondError logs an error and sends a JSON response\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {\n") helperMethods.WriteString(" logger.Error(message, map[string]interface{}{\"error\": err.Error(), \"status_code\": statusCode})\n") helperMethods.WriteString(" h.respondError(c, message, err, statusCode)\n") helperMethods.WriteString("}\n\n") helperMethods.WriteString("// respondError sends a standardized JSON error response\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) respondError(c *gin.Context, message string, err error, statusCode int) {\n") helperMethods.WriteString(" errorMessage := message\n") helperMethods.WriteString(" if gin.Mode() == gin.ReleaseMode {\n") helperMethods.WriteString(" errorMessage = \"Internal server error\"\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()})\n") helperMethods.WriteString("}\n\n") // calculateMeta method helperMethods.WriteString("// calculateMeta creates pagination metadata\n") helperMethods.WriteString("func (h *" + data.Name + "Handler) calculateMeta(limit, offset, total int) models.MetaResponse {\n") helperMethods.WriteString(" totalPages, currentPage := 0, 1\n") helperMethods.WriteString(" if limit > 0 {\n") helperMethods.WriteString(" totalPages = (total + limit - 1) / limit\n") helperMethods.WriteString(" currentPage = (offset / limit) + 1\n") helperMethods.WriteString(" }\n") helperMethods.WriteString(" return models.MetaResponse{\n") helperMethods.WriteString(" Limit: limit, Offset: offset, Total: total, TotalPages: totalPages,\n") helperMethods.WriteString(" CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0,\n") helperMethods.WriteString(" }\n") helperMethods.WriteString("}\n") return helperMethods.String() } // ================= MODEL GENERATION ===================== func generateModelFile(data HandlerData, modelDir string) { modelFileName := data.NameLower + ".go" modelFilePath := filepath.Join(modelDir, modelFileName) var importBlock string if data.Category == "models" { importBlock = `import ( "api-service/internal/models" "encoding/json" "database/sql" "time" ) ` } else { importBlock = `import ( "api-service/internal/models" "encoding/json" "database/sql" "time" ) ` } var modelContent strings.Builder modelContent.WriteString(fmt.Sprintf("package %s\n\n", data.Category)) modelContent.WriteString(importBlock) // Generate main struct dengan nullable types modelContent.WriteString(fmt.Sprintf("// %s represents the data structure for the %s table\n", data.Name, data.TableName)) modelContent.WriteString("// with proper null handling and optimized JSON marshaling\n") modelContent.WriteString(fmt.Sprintf("type %s struct {\n", data.Name)) // Track added fields to avoid duplicates addedFields := make(map[string]bool) // Add main table columns for _, col := range data.TableSchema { if addedFields[col.Name] { continue // Skip if already added } addedFields[col.Name] = true fieldName := snakeToPascal(col.Name) goType, _, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) jsonTag := snakeToCamel(col.Name) dbTag := col.Name // Gunakan nama kolom langsung jsonTagValue := jsonTag if col.Nullable { jsonTagValue += ",omitempty" } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" db:\"%s\"`\n", fieldName, goType, jsonTagValue, dbTag)) } // Add relationship columns if relationships exist if len(data.Relationships) > 0 { for _, rel := range data.Relationships { for _, col := range rel.Columns { if addedFields[col.Name] { continue // Skip if already added } addedFields[col.Name] = true fieldName := snakeToPascal(col.Name) goType, _, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) jsonTag := snakeToCamel(col.Name) dbTag := col.Name // Gunakan nama kolom langsung jsonTagValue := jsonTag if col.Nullable { jsonTagValue += ",omitempty" } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" db:\"%s\"`\n", fieldName, goType, jsonTagValue, dbTag)) } } } modelContent.WriteString("}\n\n") // Generate MarshalJSON method modelContent.WriteString(fmt.Sprintf("// Custom JSON marshaling for %s so NULL values don't appear in response\n", data.Name)) modelContent.WriteString(fmt.Sprintf("func (r %s) MarshalJSON() ([]byte, error) {\n", data.Name)) modelContent.WriteString(fmt.Sprintf(" type Alias %s\n", data.Name)) modelContent.WriteString(" aux := &struct {\n *Alias\n") // Add main table columns for _, col := range data.TableSchema { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) auxType := "*" + baseType jsonTag := snakeToCamel(col.Name) + ",omitempty" modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", fieldName, auxType, jsonTag)) } // Add relationship columns if len(data.Relationships) > 0 { for _, rel := range data.Relationships { for _, col := range rel.Columns { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) auxType := "*" + baseType jsonTag := snakeToCamel(col.Name) + ",omitempty" modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", fieldName, auxType, jsonTag)) } } } modelContent.WriteString(" }{\n Alias: (*Alias)(&r),\n }\n\n") // Add main table columns for _, col := range data.TableSchema { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, _, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n aux.%s = &r.%s.%s\n }\n", fieldName, fieldName, fieldName, valueType)) } // Add relationship columns if len(data.Relationships) > 0 { for _, rel := range data.Relationships { for _, col := range rel.Columns { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, _, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n aux.%s = &r.%s.%s\n }\n", fieldName, fieldName, fieldName, valueType)) } } } modelContent.WriteString(" return json.Marshal(aux)\n}\n\n") // Generate helper methods for main table columns for _, col := range data.TableSchema { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, baseType, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) var zeroValue string switch baseType { case "string": zeroValue = `""` case "int32", "int64", "float64": zeroValue = "0" case "bool": zeroValue = "false" case "time.Time": zeroValue = "time.Time{}" default: zeroValue = "nil" } modelContent.WriteString(fmt.Sprintf("// Helper method to safely get %s\n", fieldName)) modelContent.WriteString(fmt.Sprintf("func (r *%s) Get%s() %s {\n", data.Name, fieldName, baseType)) modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n return r.%s.%s\n }\n return %s\n}\n\n", fieldName, fieldName, valueType, zeroValue)) } // Generate helper methods for relationship columns if len(data.Relationships) > 0 { for _, rel := range data.Relationships { for _, col := range rel.Columns { if !col.Nullable { continue } fieldName := snakeToPascal(col.Name) _, baseType, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) var zeroValue string switch baseType { case "string": zeroValue = `""` case "int32", "int64", "float64": zeroValue = "0" case "bool": zeroValue = "false" case "time.Time": zeroValue = "time.Time{}" default: zeroValue = "nil" } modelContent.WriteString(fmt.Sprintf("// Helper method to safely get %s\n", fieldName)) modelContent.WriteString(fmt.Sprintf("func (r *%s) Get%s() %s {\n", data.Name, fieldName, baseType)) modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n return r.%s.%s\n }\n return %s\n}\n\n", fieldName, fieldName, valueType, zeroValue)) } } } // Generate request/response structs systemFields := findSystemFields(data.TableSchema) var createFields, updateFields []ColumnConfig for _, col := range data.TableSchema { if isSystemField(col.Name, systemFields) { continue } createFields = append(createFields, col) updateCol := col updateFields = append(updateFields, updateCol) } // Generate all response structs based on endpoints for endpointName, endpoint := range data.Endpoints { if endpoint.ResponseModel != "" { switch endpointName { case "list": modelContent.WriteString(fmt.Sprintf(`// Response struct for GET list type %sGetResponse struct { Message string `+"`json:\"message\"`"+` Data []%s `+"`json:\"data\"`"+` Meta models.MetaResponse `+"`json:\"meta\"`"+` Summary *models.AggregateData `+"`json:\"summary,omitempty\"`"+` } `, data.Name, data.Name)) case "get": primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" } modelContent.WriteString(fmt.Sprintf(`// Response struct for GET by %s type %sGetBy%sResponse struct { Message string `+"`json:\"message\"`"+` Data *%s `+"`json:\"data\"`"+` } `, primaryKey, data.Name, snakeToPascal(primaryKey), data.Name)) case "create": modelContent.WriteString(fmt.Sprintf(`// Response struct for create type %sCreateResponse struct { Message string `+"`json:\"message\"`"+` Data *%s `+"`json:\"data\"`"+` } `, data.Name, data.Name)) case "update": modelContent.WriteString(fmt.Sprintf(`// Response struct for update type %sUpdateResponse struct { Message string `+"`json:\"message\"`"+` Data *%s `+"`json:\"data\"`"+` } `, data.Name, data.Name)) case "delete": primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" } modelContent.WriteString(fmt.Sprintf(`// Response struct for delete type %sDeleteResponse struct { Message string `+"`json:\"message\"`"+` %s string `+"`json:\"%s\"`"+` } `, data.Name, snakeToPascal(primaryKey), snakeToCamel(primaryKey))) case "stats": // Stats uses AggregateData directly, no need to generate struct case "by_age": modelContent.WriteString(fmt.Sprintf(`// Response struct for by age type %sAgeStatsResponse struct { Message string `+"`json:\"message\"`"+` Data map[string]interface{} `+"`json:\"data\"`"+` } `, data.Name)) } } if endpoint.RequestModel != "" { switch endpointName { case "create": modelContent.WriteString(fmt.Sprintf("\n// Request struct for create\ntype %sCreateRequest struct {\n", data.Name)) // Add status field first if it exists statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { fieldName := snakeToPascal(statusColumn) _, baseType, _ := mapSQLTypeToGo(getColumnType(data.TableSchema, statusColumn), isColumnNullable(data.TableSchema, statusColumn), "") jsonTag := snakeToCamel(statusColumn) var requestType string if isColumnNullable(data.TableSchema, statusColumn) { requestType = "*" + baseType } else { requestType = baseType } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" validate:\"required,oneof=draft active inactive\"`\n", fieldName, requestType, jsonTag)) } for _, col := range createFields { if statusColumn != "" && col.Name == statusColumn { continue // Skip status as it's already added } fieldName := snakeToPascal(col.Name) _, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) jsonTag := snakeToCamel(col.Name) var requestType string if col.Nullable { requestType = "*" + baseType } else { requestType = baseType } // Add validation rules based on column type and validation from config validationTag := "" if col.Validation != "" { validationTag = " validate:\"" + col.Validation + "\"" } else if strings.Contains(strings.ToLower(col.Name), "nama") || strings.Contains(strings.ToLower(col.Name), "title") { validationTag = " validate:\"required,min=1,max=100\"" } else if strings.Contains(strings.ToLower(col.Name), "email") { validationTag = " validate:\"omitempty,email\"" } else if strings.Contains(strings.ToLower(col.Name), "code") || strings.Contains(strings.ToLower(col.Name), "kode") { validationTag = " validate:\"omitempty,min=1,max=50\"" } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"%s`\n", fieldName, requestType, jsonTag, validationTag)) } modelContent.WriteString("}\n\n") case "update": primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" } // Get the type of the primary key _, pkBaseType, _ := mapSQLTypeToGo(getColumnType(data.TableSchema, primaryKey), isColumnNullable(data.TableSchema, primaryKey), "") modelContent.WriteString(fmt.Sprintf("\n// Update request\ntype %sUpdateRequest struct {\n", data.Name)) // Add primary key field with correct type and tag modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"-\" validate:\"required\"`\n", snakeToPascal(primaryKey), pkBaseType)) // Add status field first if it exists statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { fieldName := snakeToPascal(statusColumn) _, baseType, _ := mapSQLTypeToGo(getColumnType(data.TableSchema, statusColumn), isColumnNullable(data.TableSchema, statusColumn), "") jsonTag := snakeToCamel(statusColumn) var requestType string if isColumnNullable(data.TableSchema, statusColumn) { requestType = "*" + baseType } else { requestType = baseType } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" validate:\"required,oneof=draft active inactive\"`\n", fieldName, requestType, jsonTag)) } for _, col := range updateFields { if statusColumn != "" && col.Name == statusColumn { continue // Skip status as it's already added } // Skip primary key as it's already added above if col.Name == primaryKey { continue } fieldName := snakeToPascal(col.Name) _, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType) jsonTag := snakeToCamel(col.Name) var requestType string if col.Nullable { requestType = "*" + baseType } else { requestType = baseType } // Add validation rules based on column type and validation from config validationTag := "" if col.Validation != "" { validationTag = " validate:\"" + col.Validation + "\"" } else if strings.Contains(strings.ToLower(col.Name), "nama") || strings.Contains(strings.ToLower(col.Name), "title") { validationTag = " validate:\"omitempty,min=1,max=255\"" } else if strings.Contains(strings.ToLower(col.Name), "email") { validationTag = " validate:\"omitempty,email\"" } else if strings.Contains(strings.ToLower(col.Name), "code") || strings.Contains(strings.ToLower(col.Name), "kode") { validationTag = " validate:\"omitempty,min=1,max=50\"" } modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"%s`\n", fieldName, requestType, jsonTag, validationTag)) } modelContent.WriteString("}\n\n") } } } if data.HasFilter { modelContent.WriteString(fmt.Sprintf("\n// Filter struct for query parameters\ntype %sFilter struct {\n", data.Name)) modelContent.WriteString(" Search *string `json:\"search,omitempty\" form:\"search\"`\n") modelContent.WriteString(" DateFrom *time.Time `json:\"date_from,omitempty\" form:\"date_from\"`\n") modelContent.WriteString(" DateTo *time.Time `json:\"date_to,omitempty\" form:\"date_to\"`\n") statusColumn := findStatusColumn(data.TableSchema) if statusColumn != "" { modelContent.WriteString(fmt.Sprintf(" Status *string `json:\"status,omitempty\" form:\"%s\"`\n", statusColumn)) } modelContent.WriteString("}\n") } writeFile(modelFilePath, modelContent.String()) fmt.Printf("Successfully generated DYNAMIC model: %s\n", modelFileName) } // MODIFIKASI: Fungsi pembantu untuk memetakan tipe SQL ke Go func mapSQLTypeToGo(sqlType string, nullable bool, explicitGoType string) (goType string, baseType string, valueType string) { sqlType = strings.ToLower(sqlType) if explicitGoType != "" { baseType = explicitGoType if nullable { switch baseType { case "string": goType, valueType = "sql.NullString", "String" case "int32": goType, valueType = "sql.NullInt32", "Int32" case "int64": goType, valueType = "sql.NullInt64", "Int64" case "bool": goType, valueType = "sql.NullBool", "Bool" case "float64": goType, valueType = "sql.NullFloat64", "Float64" case "time.Time": goType, valueType = "sql.NullTime", "Time" default: goType, valueType = "sql.NullString", "String" // fallback } } else { goType = baseType } return goType, baseType, valueType } switch { case strings.Contains(sqlType, "serial") || sqlType == "int4": if nullable { return "sql.NullInt32", "int32", "Int32" } return "int32", "int32", "" case sqlType == "int8" || sqlType == "bigint": if nullable { return "sql.NullInt64", "int64", "Int64" } return "int64", "int64", "" case sqlType == "uuid": if nullable { return "sql.NullString", "string", "String" } return "string", "string", "" case strings.Contains(sqlType, "varchar") || strings.Contains(sqlType, "text") || sqlType == "char": if nullable { return "sql.NullString", "string", "String" } return "string", "string", "" case sqlType == "bool": if nullable { return "sql.NullBool", "bool", "Bool" } return "bool", "bool", "" case strings.Contains(sqlType, "timestamp") || strings.Contains(sqlType, "date"): if nullable { return "sql.NullTime", "time.Time", "Time" } return "time.Time", "time.Time", "" case strings.Contains(sqlType, "decimal") || strings.Contains(sqlType, "numeric") || sqlType == "float8" || sqlType == "real": if nullable { return "sql.NullFloat64", "float64", "Float64" } return "float64", "float64", "" default: if nullable { return "sql.NullString", "string", "String" } return "string", "string", "" } } func snakeToPascal(s string) string { if s == "" { return "" } // Pisahkan string berdasarkan underscore parts := strings.Split(s, "_") var pascal strings.Builder for _, part := range parts { if part == "" { continue // Lewati bagian kosong jika ada double underscore } lowerPart := strings.ToLower(part) // Handle common acronyms if lowerPart == "id" { pascal.WriteString("ID") } else { // Ubah huruf pertama menjadi kapital dan sisanya kecil pascal.WriteString(strings.Title(lowerPart)) } } return pascal.String() } // snakeToCamel mengubah string dari snake_case menjadi camelCase. // Contoh: "nama_hari" -> "namaHari" func snakeToCamel(s string) string { if s == "" { return "" } parts := strings.Split(s, "_") if len(parts) == 1 { return strings.ToLower(parts[0]) } // Bagian pertama: huruf pertama kecil var camel strings.Builder camel.WriteString(strings.ToLower(parts[0])) // Bagian selanjutnya: huruf pertama besar (PascalCase) for i := 1; i < len(parts); i++ { if parts[i] == "" { continue } camel.WriteString(strings.Title(strings.ToLower(parts[i]))) } return camel.String() } // ================= ROUTES GENERATION ===================== func updateRoutesFile(data HandlerData) { routesFile := "../../internal/routes/v1/routes.go" content, err := os.ReadFile(routesFile) if err != nil { fmt.Printf("โš ๏ธ Could not read routes.go: %v\n", err) fmt.Printf("๐Ÿ“ Please manually add these routes to your routes.go file:\n") printRoutesSample(data) return } routesContent := string(content) // Clean up duplicate routes first routesContent = cleanupDuplicateRoutes(routesContent, data) // Build import path var importPath, importAlias string if data.Category != "models" { importPath = fmt.Sprintf("%s/internal/handlers/"+data.Category, data.ModuleName) importAlias = data.Category + data.Name + "Handlers" } else { importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) importAlias = data.NameLower + "Handlers" } // Add import importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath) if !strings.Contains(routesContent, importPattern) { importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath) if strings.Contains(routesContent, "import (") { routesContent = strings.Replace(routesContent, "import (", "import (\n"+importToAdd, 1) } } // Check if routes for this specific endpoint already exist if routeBlockExists(routesContent, data) { if *verboseFlag { fmt.Printf("โœ… Routes for %s (%s) already exist, skipping...\n", data.Name, getGroupPath(data)) } return } // Build new routes in protected group format newRoutes := generateProtectedRouteBlock(data) // Use the correct marker for insertion insertMarker := ` // ============================================================================= // PUBLISHED ROUTES // ============================================================================= ` // Find the position to insert routes if strings.Contains(routesContent, insertMarker) { // Insert after the marker block (including the newlines) markerIndex := strings.Index(routesContent, insertMarker) if markerIndex != -1 { // Find the end of the marker block (including the newlines) endOfMarker := markerIndex + len(insertMarker) // Insert the new routes after the marker routesContent = routesContent[:endOfMarker] + "\n" + newRoutes + routesContent[endOfMarker:] if *verboseFlag { fmt.Printf("๐Ÿ“ Inserted routes after PUBLISHED ROUTES marker\n") } } } else { // Fallback: try to find alternative markers alternativeMarkers := []string{ "// ============= PUBLISHED ROUTES ===============================================", "// PUBLISHED ROUTES", "return r", // End of function } inserted := false for _, marker := range alternativeMarkers { if strings.Contains(routesContent, marker) { if marker == "return r" { // Insert before the return statement routesContent = strings.Replace(routesContent, "\t"+marker, newRoutes+"\n\n\t"+marker, 1) } else { // Insert after the marker routesContent = strings.Replace(routesContent, marker, marker+"\n"+newRoutes, 1) } if *verboseFlag { fmt.Printf("๐Ÿ“ Inserted routes using alternative marker: %s\n", marker) } inserted = true break } } if !inserted { // Last resort: append at the end of the file routesContent += "\n" + newRoutes if *verboseFlag { fmt.Printf("๐Ÿ“ Appended routes at the end of file\n") } } } if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { logError("Error writing routes.go", err, *verboseFlag) return } if *verboseFlag { fmt.Printf("โœ… Updated routes.go with %s endpoints\n", data.Name) } } // routeBlockExists checks if a route block for the specific endpoint already exists func routeBlockExists(content string, data HandlerData) bool { groupPath := getGroupPath(data) handlerName := getHandlerName(data) // Build the expected route block pattern - lebih spesifik patterns := []string{ // Pattern 1: Full route block dengan comment (paling spesifik) fmt.Sprintf("// %s endpoints\n %sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", data.Name, handlerName, handlerName, data.Name, handlerName, groupPath), // Pattern 2: Handler dan Group declaration (tanpa comment) fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", handlerName, handlerName, data.Name, handlerName, groupPath), // Pattern 3: Group declaration saja (jika handler sudah ada di import) fmt.Sprintf("%sGroup := v1.Group(\"/%s\")", handlerName, groupPath), // Pattern 4: Cari kombinasi handler dan group dengan spasi yang berbeda fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name), } // Check if any of the patterns exist for _, pattern := range patterns { if strings.Contains(content, pattern) { if *verboseFlag { fmt.Printf("๐Ÿ” Found existing route pattern: %s\n", pattern) } return true } } // Additional check: look for any route with the same group path groupPattern := fmt.Sprintf("v1.Group(\"/%s\")", groupPath) if strings.Contains(content, groupPattern) { if *verboseFlag { fmt.Printf("๐Ÿ” Found existing group pattern: %s\n", groupPattern) } return true } return false } // getGroupPath returns the group path for the given data func getGroupPath(data HandlerData) string { if data.Category != "models" { return strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) } return strings.ToLower(data.Name) } // getHandlerName returns the handler name for the given data func getHandlerName(data HandlerData) string { if data.Category != "models" { return data.Category + data.Name } return strings.ToLower(data.Name) } func generateProtectedRouteBlock(data HandlerData) string { var sb strings.Builder handlerName := getHandlerName(data) groupPath := getGroupPath(data) primaryKey := findPrimaryKey(data.TableSchema) if primaryKey == "" { primaryKey = "id" // Default fallback } // Komentar dan deklarasi handler & grup sb.WriteString("// ") sb.WriteString(data.Name) sb.WriteString(" endpoints\n") sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Handler := ") sb.WriteString(handlerName) sb.WriteString("Handlers.New") sb.WriteString(data.Name) sb.WriteString("Handler()\n ") sb.WriteString(handlerName) sb.WriteString("Group := v1.Group(\"/") sb.WriteString(groupPath) sb.WriteString("\")\n {\n") // Generate routes for all endpoints for _, endpoint := range data.Endpoints { for _, method := range endpoint.Methods { switch strings.ToLower(method) { case "get": if endpoint.Path == "/:"+primaryKey { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/:") sb.WriteString(primaryKey) sb.WriteString("\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("By") sb.WriteString(snakeToPascal(primaryKey)) sb.WriteString(")\n") } else if endpoint.Path == "/dynamic" { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/dynamic\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Dynamic)\n") } else if endpoint.Path == "/search" { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/search\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Search") sb.WriteString(data.Name) sb.WriteString(")\n") } else if endpoint.Path == "/stats" { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/stats\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Stats)\n") } else if endpoint.Path == "/by-location" { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/by-location\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("ByLocation)\n") } else if endpoint.Path == "/by-age" { sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/by-age\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("ByAge)\n") } else { // Default GET handler sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.GET(\"") sb.WriteString(endpoint.Path) sb.WriteString("\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString(")\n") } case "post": sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.POST(\"") sb.WriteString(endpoint.Path) sb.WriteString("\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Create") sb.WriteString(data.Name) sb.WriteString(")\n") case "put": sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.PUT(\"/:") sb.WriteString(primaryKey) sb.WriteString("\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Update") sb.WriteString(data.Name) sb.WriteString(")\n") case "delete": sb.WriteString(" ") sb.WriteString(handlerName) sb.WriteString("Group.DELETE(\"/:") sb.WriteString(primaryKey) sb.WriteString("\", ") sb.WriteString(handlerName) sb.WriteString("Handler.Delete") sb.WriteString(data.Name) sb.WriteString(")\n") } } } sb.WriteString(" }\n") return sb.String() } func cleanupDuplicateRoutes(content string, data HandlerData) string { // Implement getGroupPath logic directly var groupPath string if data.Category != "models" { groupPath = strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) } else { groupPath = strings.ToLower(data.Name) } // Implement getHandlerName logic directly var handlerName string if data.Category != "models" { handlerName = data.Category + data.Name } else { handlerName = strings.ToLower(data.Name) } // Split content by lines for better processing lines := strings.Split(content, "\n") var cleanedLines []string inRouteBlock := false routeBlockFound := false blockStartLine := -1 for i, line := range lines { // Check if this line starts a route block for our endpoint // Look for comment pattern first if strings.Contains(line, fmt.Sprintf("// %s endpoints", data.Name)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate route block for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // Alternative pattern: look for handler declaration without comment if !inRouteBlock && strings.Contains(line, fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate handler declaration for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // Third pattern: look for group declaration (for cases without handler declaration on same line) if !inRouteBlock && strings.Contains(line, fmt.Sprintf("Group := v1.Group(\"/%s\")", groupPath)) { if routeBlockFound { // This is a duplicate, skip it if *verboseFlag { fmt.Printf("๐Ÿงน Skipping duplicate group declaration for %s at line %d\n", data.Name, i+1) } inRouteBlock = true blockStartLine = i continue } else { // This is the first occurrence, keep it routeBlockFound = true inRouteBlock = true blockStartLine = i cleanedLines = append(cleanedLines, line) continue } } // If we're in a route block, check if we've reached the end if inRouteBlock { // Count braces to determine if we're at the end of the block openBraces := strings.Count(line, "{") closeBraces := strings.Count(line, "}") // If we have more closing braces than opening braces, we might be at the end if closeBraces > openBraces { // Check if this line contains the closing brace for the route group if strings.Contains(line, "}") && (strings.Contains(line, "Group") || strings.Contains(line, "handler")) { // This is likely the end of our route block if routeBlockFound && blockStartLine != -1 { cleanedLines = append(cleanedLines, line) inRouteBlock = false blockStartLine = -1 continue } } } // Additional check: look for standalone closing brace that might end the block if strings.TrimSpace(line) == "}" && inRouteBlock { // This might be the end of our route block if routeBlockFound && blockStartLine != -1 { cleanedLines = append(cleanedLines, line) inRouteBlock = false blockStartLine = -1 continue } } // Add the line if we're keeping this block if routeBlockFound { cleanedLines = append(cleanedLines, line) } } else { // Not in a route block, just add the line cleanedLines = append(cleanedLines, line) } } return strings.Join(cleanedLines, "\n") } func printRoutesSample(data HandlerData) { fmt.Print(generateProtectedRouteBlock(data)) fmt.Println() } // ================= UTILITY FUNCTIONS ===================== func writeFile(filename, content string) { if err := os.WriteFile(filename, []byte(content), 0644); err != nil { logError(fmt.Sprintf("Error creating file %s", filename), err, *verboseFlag) return } if *verboseFlag { fmt.Printf("โœ… Generated: %s\n", filename) } } // Enhanced error logging function func logError(message string, err error, verbose bool) { if verbose { log.Printf("โŒ ERROR: %s - %v", message, err) } else { log.Printf("โŒ ERROR: %s", message) } } // Success logging function func logSuccess(message string, details ...string) { fmt.Printf("โœ… %s", message) for _, detail := range details { fmt.Printf(" - %s", detail) } fmt.Println() } // ================= HELPER FUNCTIONS ===================== // findPrimaryKey finds the primary key column from schema func findPrimaryKey(columns []ColumnConfig) string { for _, col := range columns { if col.PrimaryKey { return col.Name } } return "id" // Default fallback } // findSearchableColumns finds columns that can be used for searching func findSearchableColumns(columns []ColumnConfig) []string { var searchable []string for _, col := range columns { if col.Searchable { searchable = append(searchable, col.Name) } else { // Auto-detect searchable columns based on name colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "nama") || strings.Contains(colNameLower, "name") || strings.Contains(colNameLower, "title") || strings.Contains(colNameLower, "nomr") || strings.Contains(colNameLower, "code") || strings.Contains(colNameLower, "kode") { searchable = append(searchable, col.Name) } } } return searchable } // findUniqueColumns finds columns that must be unique func findUniqueColumns(columns []ColumnConfig) []string { var unique []string for _, col := range columns { if col.Unique { unique = append(unique, col.Name) } else { // Auto-detect unique columns based on name colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "nomr") || strings.Contains(colNameLower, "no_kartu") || strings.Contains(colNameLower, "kode") || strings.Contains(colNameLower, "code") { unique = append(unique, col.Name) } } } return unique } // findStatusColumn finds the status column from schema func findStatusColumn(columns []ColumnConfig) string { for _, col := range columns { colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "status") { return col.Name } } return "status" // Default fallback } // findSystemFields finds system fields that should be excluded from user input func findSystemFields(columns []ColumnConfig) []string { var systemFields []string for _, col := range columns { if col.SystemField { systemFields = append(systemFields, col.Name) } else { // Auto-detect system fields based on name colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "date_created") || strings.Contains(colNameLower, "date_updated") || strings.Contains(colNameLower, "user_created") || strings.Contains(colNameLower, "user_updated") || strings.Contains(colNameLower, "created_at") || strings.Contains(colNameLower, "updated_at") { systemFields = append(systemFields, col.Name) } } } return systemFields } // findLocationColumns finds columns related to location func findLocationColumns(columns []ColumnConfig) []string { var locationColumns []string for _, col := range columns { colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "provinsi") || strings.Contains(colNameLower, "kota") || strings.Contains(colNameLower, "kecamatan") || strings.Contains(colNameLower, "kelurahan") { locationColumns = append(locationColumns, col.Name) } } return locationColumns } // findBirthDateColumn finds the birth date column func findBirthDateColumn(columns []ColumnConfig) string { for _, col := range columns { colNameLower := strings.ToLower(col.Name) if strings.Contains(colNameLower, "tgl_lahir") || strings.Contains(colNameLower, "birth_date") || strings.Contains(colNameLower, "tanggal_lahir") { return col.Name } } return "" // Return empty if not found } // isSystemField checks if a column is a system field func isSystemField(columnName string, systemFields []string) bool { for _, field := range systemFields { if field == columnName { return true } } return false } // isAutoGeneratedPrimaryKey checks if the primary key is auto-generated func isAutoGeneratedPrimaryKey(columns []ColumnConfig) bool { for _, col := range columns { if col.PrimaryKey { return strings.Contains(strings.ToLower(col.Type), "serial") || strings.Contains(strings.ToLower(col.Type), "uuid") } } return false } // getColumnType returns the type of a column func getColumnType(columns []ColumnConfig, columnName string) string { for _, col := range columns { if col.Name == columnName { return col.Type } } return "" } // isColumnNullable checks if a column is nullable func isColumnNullable(columns []ColumnConfig, columnName string) bool { for _, col := range columns { if col.Name == columnName { return col.Nullable } } return false } // getFieldsForEndpoint returns the list of fields to select for an endpoint func getFieldsForEndpoint(data HandlerData, fieldsGroup string) []string { // Check if the fields group is defined in the configuration if fieldsGroup != "" { // For now, we'll handle the predefined field groups switch fieldsGroup { case "base_fields": return getBaseFields(data.TableSchema) case "location_fields": return getLocationFields(data.TableSchema) case "identity_fields": return getIdentityFields(data.TableSchema) case "all_fields": return getAllFields(data.TableSchema) case "with_location_names": return getFieldsWithLocationNames(data) default: // If it's not a predefined group, treat it as a comma-separated list of fields return strings.Split(fieldsGroup, ",") } } // Default to all fields if no specific group is defined return getAllFields(data.TableSchema) } // Helper functions to get specific field groups func getBaseFields(columns []ColumnConfig) []string { // Implementation to extract base fields from columns var fields []string for _, col := range columns { if !isSystemField(col.Name, findSystemFields(columns)) { fields = append(fields, col.Name) } } return fields } func getLocationFields(columns []ColumnConfig) []string { // Implementation to extract location fields from columns var fields []string locationFields := []string{"alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi"} for _, col := range columns { for _, locField := range locationFields { if col.Name == locField { fields = append(fields, col.Name) break } } } return fields } func getIdentityFields(columns []ColumnConfig) []string { // Implementation to extract identity fields from columns var fields []string identityFields := []string{"agama", "no_kartu", "noktp_baru"} for _, col := range columns { for _, idField := range identityFields { if col.Name == idField { fields = append(fields, col.Name) break } } } return fields } func getAllFields(columns []ColumnConfig) []string { // Implementation to extract all non-system fields from columns var fields []string systemFields := findSystemFields(columns) for _, col := range columns { if !isSystemField(col.Name, systemFields) { fields = append(fields, col.Name) } } return fields } func getFieldsWithLocationNames(data HandlerData) []string { // Implementation to extract fields with location names // This would include both the location IDs and their corresponding names var fields []string systemFields := findSystemFields(data.TableSchema) // Add all non-system fields for _, col := range data.TableSchema { if !isSystemField(col.Name, systemFields) { fields = append(fields, col.Name) } } // Add location name fields (these would be joined from other tables) locationNameFields := []string{"namakelurahan", "namakecamatan", "namakota", "namaprovinsi"} for _, nameField := range locationNameFields { fields = append(fields, nameField) } return fields } // hasRelationshipFields checks if any field from relationships is included in the fields list func hasRelationshipFields(fields []string, relationships []RelationshipConfig) bool { for _, field := range fields { for _, rel := range relationships { for _, col := range rel.Columns { if field == col.Name { return true } } } } return false } // hasRelationshipField checks if a specific field from a relationship is included in the fields list func hasRelationshipField(fields []string, relationship RelationshipConfig) bool { for _, field := range fields { for _, col := range relationship.Columns { if field == col.Name { return true } } } return false }