4176 lines
190 KiB
Go
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
|
|
}
|