Files
api_antrean/tools/general/generate-handler.go
2025-11-03 05:56:41 +00:00

4176 lines
190 KiB
Go

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
}