2627 lines
85 KiB
Go
2627 lines
85 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
|
|
HasGet bool
|
|
HasPost bool
|
|
HasPut bool
|
|
HasDelete bool
|
|
HasStats bool
|
|
HasDynamic bool
|
|
HasSearch bool
|
|
HasFilter bool
|
|
HasPagination bool
|
|
Timestamp string
|
|
}
|
|
|
|
// Config represents the YAML configuration structure
|
|
type Config struct {
|
|
Entities []Entity `yaml:"entities"`
|
|
}
|
|
|
|
// Entity represents a single entity configuration in YAML
|
|
type Entity struct {
|
|
Name string `yaml:"name"`
|
|
Methods []string `yaml:"methods"`
|
|
Category string `yaml:"category,omitempty"`
|
|
}
|
|
|
|
// ServicesConfig represents the new services-based YAML configuration structure
|
|
type ServicesConfig struct {
|
|
Global GlobalConfig `yaml:"global"`
|
|
Services map[string]ServiceConfig `yaml:"services"`
|
|
}
|
|
|
|
// GlobalConfig represents global configuration
|
|
type GlobalConfig struct {
|
|
ModuleName string `yaml:"module_name"`
|
|
OutputDir string `yaml:"output_dir"`
|
|
EnableSwagger bool `yaml:"enable_swagger"`
|
|
EnableLogging bool `yaml:"enable_logging"`
|
|
}
|
|
|
|
// ServiceConfig represents a service configuration
|
|
type ServiceConfig struct {
|
|
Name string `yaml:"name"`
|
|
Category string `yaml:"category"`
|
|
Package string `yaml:"package"`
|
|
Description string `yaml:"description"`
|
|
BaseURL string `yaml:"base_url"`
|
|
Timeout int `yaml:"timeout"`
|
|
RetryCount int `yaml:"retry_count"`
|
|
Endpoints map[string]EndpointConfig `yaml:"endpoints"`
|
|
}
|
|
|
|
// EndpointConfig represents an endpoint configuration
|
|
type EndpointConfig struct {
|
|
Description string `yaml:"description"`
|
|
HandlerFolder string `yaml:"handler_folder"`
|
|
HandlerFile string `yaml:"handler_file"`
|
|
HandlerName string `yaml:"handler_name"`
|
|
TableName string `yaml:"table_name,omitempty"`
|
|
Functions map[string]FunctionConfig `yaml:"functions"`
|
|
}
|
|
|
|
// FunctionConfig represents a function configuration
|
|
type FunctionConfig struct {
|
|
Methods []string `yaml:"methods"`
|
|
Path string `yaml:"path"`
|
|
GetRoutes string `yaml:"get_routes,omitempty"`
|
|
PostRoutes string `yaml:"post_routes,omitempty"`
|
|
PutRoutes string `yaml:"put_routes,omitempty"`
|
|
DeleteRoutes string `yaml:"delete_routes,omitempty"`
|
|
GetPath string `yaml:"get_path,omitempty"`
|
|
PostPath string `yaml:"post_path,omitempty"`
|
|
PutPath string `yaml:"put_path,omitempty"`
|
|
DeletePath string `yaml:"delete_path,omitempty"`
|
|
Model string `yaml:"model"`
|
|
ResponseModel string `yaml:"response_model"`
|
|
RequestModel string `yaml:"request_model,omitempty"`
|
|
Description string `yaml:"description"`
|
|
Summary string `yaml:"summary"`
|
|
Tags []string `yaml:"tags"`
|
|
RequireAuth bool `yaml:"require_auth"`
|
|
CacheEnabled bool `yaml:"cache_enabled"`
|
|
EnableDatabase bool `yaml:"enable_database"`
|
|
CacheTTL int `yaml:"cache_ttl"`
|
|
HasPagination bool `yaml:"has_pagination,omitempty"`
|
|
HasFilter bool `yaml:"has_filter,omitempty"`
|
|
HasSearch bool `yaml:"has_search,omitempty"`
|
|
HasStats bool `yaml:"has_stats,omitempty"`
|
|
HasDynamic bool `yaml:"has_dynamic,omitempty"`
|
|
}
|
|
|
|
type PathInfo struct {
|
|
Category string
|
|
EntityName string
|
|
DirPath string
|
|
FilePath string
|
|
}
|
|
|
|
// Global variables for command line flags
|
|
var (
|
|
forceFlag = flag.Bool("force", false, "Force overwrite existing files")
|
|
verboseFlag = flag.Bool("verbose", false, "Enable verbose output")
|
|
configFlag = flag.String("config", "", "Specify config file path")
|
|
)
|
|
|
|
// Global file skip function
|
|
var shouldSkipExistingFile func(filePath string, fileType string) bool
|
|
|
|
// parseEntityPath - Enhanced logic parsing dengan validasi lebih baik
|
|
func parseEntityPath(entityPath string) (*PathInfo, error) {
|
|
if strings.TrimSpace(entityPath) == "" {
|
|
return nil, fmt.Errorf("entity path cannot be empty")
|
|
}
|
|
|
|
var pathInfo PathInfo
|
|
|
|
// Clean path untuk menghapus leading/trailing slashes
|
|
cleanedPath := strings.Trim(entityPath, "/")
|
|
parts := strings.Split(cleanedPath, "/")
|
|
|
|
// Validasi minimal 1 bagian (file saja) dan maksimal 4
|
|
if len(parts) < 1 || len(parts) > 4 {
|
|
return nil, fmt.Errorf("invalid path format: use up to 4 levels like 'level1/level2/level3/entity'")
|
|
}
|
|
|
|
// Validasi bagian kosong dan karakter tidak valid
|
|
for i, part := range parts {
|
|
if strings.TrimSpace(part) == "" {
|
|
return nil, fmt.Errorf("empty path segment at position %d", i+1)
|
|
}
|
|
|
|
// Validasi karakter untuk keamanan
|
|
if !isValidPathSegment(part) {
|
|
return nil, fmt.Errorf("invalid characters in path segment '%s' at position %d", part, i+1)
|
|
}
|
|
}
|
|
|
|
pathInfo.EntityName = parts[len(parts)-1]
|
|
|
|
if len(parts) > 1 {
|
|
pathInfo.Category = parts[len(parts)-2]
|
|
pathInfo.DirPath = strings.Join(parts[:len(parts)-1], "/")
|
|
pathInfo.FilePath = pathInfo.DirPath + "/" + strings.ToLower(pathInfo.EntityName) + ".go"
|
|
} else {
|
|
pathInfo.Category = "models"
|
|
pathInfo.DirPath = ""
|
|
pathInfo.FilePath = strings.ToLower(pathInfo.EntityName) + ".go"
|
|
}
|
|
|
|
return &pathInfo, nil
|
|
}
|
|
|
|
// Validasi karakter untuk path segment
|
|
func isValidPathSegment(segment string) bool {
|
|
// Hanya izinkan alphanumeric, underscore, dan dash
|
|
for _, char := range segment {
|
|
if !((char >= 'a' && char <= 'z') ||
|
|
(char >= 'A' && char <= 'Z') ||
|
|
(char >= '0' && char <= '9') ||
|
|
char == '_' || char == '-') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// validateMethods - Validasi method yang diinput
|
|
func validateMethods(methods []string) error {
|
|
validMethods := map[string]bool{
|
|
"get": true, "post": true, "put": true, "delete": true,
|
|
"stats": true, "dynamic": true, "search": true,
|
|
}
|
|
|
|
for _, method := range methods {
|
|
if !validMethods[strings.ToLower(method)] {
|
|
return fmt.Errorf("invalid method: %s. Valid methods: get, post, put, delete, stats, dynamic, search", method)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// generateTableName - Generate table name berdasarkan path lengkap
|
|
func generateTableName(pathInfo *PathInfo) string {
|
|
entityLower := strings.ToLower(pathInfo.EntityName)
|
|
|
|
if pathInfo.DirPath != "" {
|
|
// Replace "/" dengan "_" untuk table name
|
|
pathForTable := strings.ReplaceAll(pathInfo.DirPath, "/", "_")
|
|
return "data_" + pathForTable + "_" + entityLower
|
|
}
|
|
return "data_" + entityLower
|
|
}
|
|
|
|
// createDirectories - Enhanced directory creation dengan better error handling
|
|
func createDirectories(pathInfo *PathInfo) (string, string, error) {
|
|
var handlerDir, modelDir string
|
|
|
|
// Support nested directories lebih baik
|
|
if pathInfo.DirPath != "" {
|
|
// Normalize path untuk memastikan konsistensi
|
|
normalizedPath := filepath.Clean(pathInfo.DirPath)
|
|
handlerDir = filepath.Join("internal", "handlers", normalizedPath)
|
|
modelDir = filepath.Join("internal", "models", normalizedPath)
|
|
} else {
|
|
handlerDir = filepath.Join("internal", "handlers")
|
|
modelDir = filepath.Join("internal", "models")
|
|
}
|
|
|
|
// Create directories dengan permission yang tepat
|
|
for _, dir := range []string{handlerDir, modelDir} {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return "", "", fmt.Errorf("failed to create directory %s: %v", dir, err)
|
|
}
|
|
if *verboseFlag {
|
|
fmt.Printf("📁 Created directory: %s\n", dir)
|
|
}
|
|
}
|
|
|
|
return handlerDir, modelDir, nil
|
|
}
|
|
|
|
// setMethods - Set method flags berdasarkan input
|
|
func setMethods(data *HandlerData, methods []string) {
|
|
methodMap := map[string]*bool{
|
|
"get": &data.HasGet,
|
|
"post": &data.HasPost,
|
|
"put": &data.HasPut,
|
|
"delete": &data.HasDelete,
|
|
"stats": &data.HasStats,
|
|
"dynamic": &data.HasDynamic,
|
|
"search": &data.HasSearch,
|
|
}
|
|
|
|
for _, method := range methods {
|
|
if flag, exists := methodMap[strings.ToLower(method)]; exists {
|
|
*flag = true
|
|
}
|
|
}
|
|
|
|
// Always add stats if we have get
|
|
if data.HasGet {
|
|
data.HasStats = true
|
|
}
|
|
}
|
|
|
|
// Helper function untuk remove duplicate methods
|
|
func removeDuplicateMethods(methods []string) []string {
|
|
seen := make(map[string]bool)
|
|
result := []string{}
|
|
|
|
for _, method := range methods {
|
|
lowerMethod := strings.ToLower(method)
|
|
if !seen[lowerMethod] {
|
|
seen[lowerMethod] = true
|
|
result = append(result, method)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Validasi apakah file sudah ada dan harus di-skip
|
|
func defaultShouldSkipExistingFile(filePath string, fileType string) bool {
|
|
if *forceFlag {
|
|
if *verboseFlag {
|
|
fmt.Printf("🔄 Force mode enabled, overwriting: %s\n", filePath)
|
|
}
|
|
return false
|
|
}
|
|
|
|
if _, err := os.Stat(filePath); err == nil {
|
|
fmt.Printf("⚠️ %s file already exists: %s\n", strings.Title(fileType), filePath)
|
|
if *verboseFlag {
|
|
fmt.Printf(" Use --force to overwrite\n")
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func loadConfig(configPath string) (*Config, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
var config Config
|
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse YAML config: %w", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
func loadServicesConfig(configPath string) (*ServicesConfig, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read services config file: %w", err)
|
|
}
|
|
|
|
var config ServicesConfig
|
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse YAML services config: %w", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// generateFromServicesConfig - RESTRUCTURED untuk agreggasi methods
|
|
func generateFromServicesConfig(config *ServicesConfig) {
|
|
for serviceName, service := range config.Services {
|
|
if *verboseFlag {
|
|
fmt.Printf("🔧 Processing service: %s\n", serviceName)
|
|
}
|
|
|
|
for endpointName, endpoint := range service.Endpoints {
|
|
if *verboseFlag {
|
|
fmt.Printf(" 📍 Processing endpoint: %s\n", endpointName)
|
|
}
|
|
|
|
// Parse entity path dari endpoint name
|
|
pathInfo, err := parseEntityPath(endpointName)
|
|
if err != nil {
|
|
logError(fmt.Sprintf("Error parsing entity path '%s'", endpointName), err, *verboseFlag)
|
|
continue
|
|
}
|
|
|
|
// Override category dari service config
|
|
if service.Category != "" {
|
|
pathInfo.Category = service.Category
|
|
}
|
|
|
|
// Set directory path dari handler_folder jika specified
|
|
if endpoint.HandlerFolder != "" {
|
|
pathInfo.DirPath = endpoint.HandlerFolder
|
|
}
|
|
|
|
// AGGREGATE semua methods dari semua functions
|
|
var allMethods []string
|
|
var functionConfigs []FunctionConfig
|
|
|
|
for functionName, function := range endpoint.Functions {
|
|
if *verboseFlag {
|
|
fmt.Printf(" ⚙️ Processing function: %s\n", functionName)
|
|
}
|
|
|
|
// Tambahkan methods dari function ini
|
|
allMethods = append(allMethods, function.Methods...)
|
|
functionConfigs = append(functionConfigs, function)
|
|
}
|
|
|
|
// Remove duplicates dari methods
|
|
allMethods = removeDuplicateMethods(allMethods)
|
|
|
|
// Jika tidak ada methods, gunakan default
|
|
if len(allMethods) == 0 {
|
|
allMethods = []string{"get", "post", "put", "delete", "dynamic", "search"}
|
|
}
|
|
|
|
// Validate methods
|
|
if err := validateMethods(allMethods); err != nil {
|
|
logError(fmt.Sprintf("Invalid methods for endpoint '%s'", endpointName), err, *verboseFlag)
|
|
continue
|
|
}
|
|
|
|
// Override table name jika specified
|
|
tableName := endpoint.TableName
|
|
if tableName == "" {
|
|
tableName = generateTableName(pathInfo)
|
|
}
|
|
|
|
// Generate handler data dengan service-specific information
|
|
entityName := strings.Title(pathInfo.EntityName)
|
|
entityLower := strings.ToLower(pathInfo.EntityName)
|
|
entityPlural := entityLower + "s"
|
|
|
|
data := HandlerData{
|
|
Name: entityName,
|
|
NameLower: entityLower,
|
|
NamePlural: entityPlural,
|
|
Category: pathInfo.Category,
|
|
DirPath: pathInfo.DirPath,
|
|
ModuleName: config.Global.ModuleName,
|
|
TableName: tableName,
|
|
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
|
|
}
|
|
|
|
// Set methods berdasarkan aggregated methods
|
|
setMethods(&data, allMethods)
|
|
|
|
// Set flags berdasarkan function configs
|
|
for _, function := range functionConfigs {
|
|
if function.HasPagination {
|
|
data.HasPagination = true
|
|
}
|
|
if function.HasFilter {
|
|
data.HasFilter = true
|
|
}
|
|
if function.HasSearch {
|
|
data.HasSearch = true
|
|
}
|
|
if function.HasStats {
|
|
data.HasStats = true
|
|
}
|
|
if function.HasDynamic {
|
|
data.HasDynamic = true
|
|
}
|
|
}
|
|
|
|
// Create directories
|
|
handlerDir, modelDir, err := createDirectories(pathInfo)
|
|
if err != nil {
|
|
logError("Error creating directories", err, *verboseFlag)
|
|
continue
|
|
}
|
|
|
|
// CHECK existing files sebelum generate
|
|
handlerPath := filepath.Join(handlerDir, entityLower+".go")
|
|
modelPath := filepath.Join(modelDir, entityLower+".go")
|
|
|
|
if shouldSkipExistingFile(handlerPath, "handler") {
|
|
fmt.Printf("⚠️ Skipping handler generation: %s\n", handlerPath)
|
|
continue
|
|
}
|
|
|
|
if shouldSkipExistingFile(modelPath, "model") {
|
|
fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath)
|
|
continue
|
|
}
|
|
|
|
// Generate files (SEKALI SAJA per endpoint)
|
|
generateHandlerFile(data, handlerDir)
|
|
generateModelFile(data, modelDir)
|
|
|
|
// HANYA UPDATE ROUTES SEKALI PER ENDPOINT setelah semua fungsi di-aggregate
|
|
updateRoutesFile(data)
|
|
|
|
// Success output
|
|
logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName),
|
|
fmt.Sprintf("Category: %s", pathInfo.Category),
|
|
fmt.Sprintf("Path: %s", pathInfo.DirPath),
|
|
fmt.Sprintf("Handler: %s", handlerPath),
|
|
fmt.Sprintf("Model: %s", modelPath),
|
|
fmt.Sprintf("Table: %s", tableName),
|
|
fmt.Sprintf("Methods: %s", strings.Join(allMethods, ", ")),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
// Parse command line flags
|
|
flag.Parse()
|
|
|
|
// Set global file skip function
|
|
shouldSkipExistingFile = defaultShouldSkipExistingFile
|
|
|
|
// Determine config file path
|
|
configPath := "services-config.yaml"
|
|
if *configFlag != "" {
|
|
configPath = *configFlag
|
|
}
|
|
|
|
// Check for services-config.yaml first (new format)
|
|
servicesConfig, err := loadServicesConfig(configPath)
|
|
if err == nil {
|
|
// Use services config
|
|
if *verboseFlag {
|
|
fmt.Printf("📄 Using services configuration from %s\n", configPath)
|
|
}
|
|
generateFromServicesConfig(servicesConfig)
|
|
return
|
|
}
|
|
|
|
// Fallback to old config-handler.yml
|
|
oldConfigPath := "config-handler.yml"
|
|
if *configFlag == "" {
|
|
oldConfigPath = "config-handler.yml"
|
|
}
|
|
|
|
config, err := loadConfig(oldConfigPath)
|
|
if err == nil {
|
|
// Generate from old config
|
|
if *verboseFlag {
|
|
fmt.Printf("📄 Using legacy configuration from %s\n", oldConfigPath)
|
|
}
|
|
for _, entity := range config.Entities {
|
|
pathInfo, err := parseEntityPath(entity.Name)
|
|
if err != nil {
|
|
logError(fmt.Sprintf("Error parsing entity path '%s'", entity.Name), err, *verboseFlag)
|
|
continue
|
|
}
|
|
|
|
// Override category if specified in config
|
|
if entity.Category != "" {
|
|
pathInfo.Category = entity.Category
|
|
}
|
|
|
|
// Use methods from config or default
|
|
methods := entity.Methods
|
|
if len(methods) == 0 {
|
|
methods = []string{"get", "post", "put", "delete", "dynamic", "search"}
|
|
}
|
|
|
|
// Validate methods
|
|
if err := validateMethods(methods); err != nil {
|
|
logError(fmt.Sprintf("Invalid methods for entity '%s'", entity.Name), err, *verboseFlag)
|
|
continue
|
|
}
|
|
|
|
generateForEntity(pathInfo, methods)
|
|
}
|
|
return
|
|
}
|
|
|
|
// No config files found, fallback to command line arguments
|
|
fmt.Printf("⚠️ No config files found (%s or %s), falling back to command line arguments\n", configPath, oldConfigPath)
|
|
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: go run generate-handler.go [path/]entity [methods]")
|
|
fmt.Println("Examples:")
|
|
fmt.Println(" go run generate-handler.go product get post put delete")
|
|
fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search")
|
|
fmt.Println(" go run generate-handler.go product/category/subcategory/item get post")
|
|
fmt.Println("\nSupported methods: get, post, put, delete, stats, dynamic, search")
|
|
fmt.Println("\nAlternatively, create a services-config.yaml or config-handler.yml file with configurations.")
|
|
fmt.Println("\nFlags:")
|
|
fmt.Println(" --force Force overwrite existing files")
|
|
fmt.Println(" --verbose Enable verbose output")
|
|
fmt.Println(" --config Specify config file path")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Parse entity path
|
|
entityPath := strings.TrimSpace(os.Args[1])
|
|
pathInfo, err := parseEntityPath(entityPath)
|
|
if err != nil {
|
|
logError("Error parsing path", err, true)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Parse methods
|
|
var methods []string
|
|
if len(os.Args) > 2 {
|
|
methods = os.Args[2:]
|
|
} else {
|
|
// Default methods with advanced features
|
|
methods = []string{"get", "post", "put", "delete", "dynamic", "search"}
|
|
}
|
|
|
|
// Validate methods
|
|
if err := validateMethods(methods); err != nil {
|
|
logError("Method validation failed", err, true)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Generate for single entity
|
|
generateForEntity(pathInfo, methods)
|
|
}
|
|
|
|
func generateForEntity(pathInfo *PathInfo, methods []string) {
|
|
// Format names
|
|
entityName := strings.Title(pathInfo.EntityName) // PascalCase entity name
|
|
entityLower := strings.ToLower(pathInfo.EntityName)
|
|
entityPlural := entityLower + "s"
|
|
|
|
// Generate table name
|
|
tableName := generateTableName(pathInfo)
|
|
|
|
// Create HandlerData
|
|
data := HandlerData{
|
|
Name: entityName,
|
|
NameLower: entityLower,
|
|
NamePlural: entityPlural,
|
|
Category: pathInfo.Category,
|
|
DirPath: pathInfo.DirPath,
|
|
ModuleName: "api-service",
|
|
TableName: tableName,
|
|
HasPagination: true,
|
|
HasFilter: true,
|
|
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
|
|
}
|
|
|
|
// Set methods
|
|
setMethods(&data, methods)
|
|
|
|
// Create directories
|
|
handlerDir, modelDir, err := createDirectories(pathInfo)
|
|
if err != nil {
|
|
logError("Error creating directories", err, *verboseFlag)
|
|
return
|
|
}
|
|
|
|
// CHECK existing files sebelum generate
|
|
handlerPath := filepath.Join(handlerDir, entityLower+".go")
|
|
modelPath := filepath.Join(modelDir, entityLower+".go")
|
|
|
|
if shouldSkipExistingFile(handlerPath, "handler") {
|
|
fmt.Printf("⚠️ Skipping handler generation: %s\n", handlerPath)
|
|
return
|
|
}
|
|
|
|
if shouldSkipExistingFile(modelPath, "model") {
|
|
fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath)
|
|
return
|
|
}
|
|
|
|
// Generate files
|
|
generateHandlerFile(data, handlerDir)
|
|
generateModelFile(data, modelDir)
|
|
updateRoutesFile(data) // Ini untuk legacy mode, masih ok karena hanya sekali per entity
|
|
|
|
// Success output
|
|
logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName),
|
|
fmt.Sprintf("Category: %s", pathInfo.Category),
|
|
fmt.Sprintf("Path: %s", pathInfo.DirPath),
|
|
fmt.Sprintf("Handler: %s", handlerPath),
|
|
fmt.Sprintf("Model: %s", modelPath),
|
|
fmt.Sprintf("Table: %s", tableName),
|
|
fmt.Sprintf("Methods: %s", strings.Join(methods, ", ")),
|
|
)
|
|
}
|
|
|
|
// ================= HANDLER GENERATION =====================
|
|
func generateHandlerFile(data HandlerData, handlerDir string) {
|
|
var handlerContent strings.Builder
|
|
|
|
// Header
|
|
handlerContent.WriteString("package handlers\n\n")
|
|
handlerContent.WriteString("import (\n")
|
|
handlerContent.WriteString(` "` + data.ModuleName + `/internal/config"` + "\n")
|
|
handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n")
|
|
handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n")
|
|
if data.Category != "models" {
|
|
handlerContent.WriteString(data.Category + `Models "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n")
|
|
}
|
|
|
|
// Conditional imports based on enabled methods
|
|
if data.HasDynamic || data.HasSearch {
|
|
handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n")
|
|
}
|
|
|
|
// Only import validation if POST is enabled (since validation is primarily for create operations)
|
|
if data.HasPost {
|
|
handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\n")
|
|
}
|
|
|
|
handlerContent.WriteString(` "context"` + "\n")
|
|
handlerContent.WriteString(` "database/sql"` + "\n")
|
|
handlerContent.WriteString(` "fmt"` + "\n")
|
|
handlerContent.WriteString(` "log"` + "\n")
|
|
handlerContent.WriteString(` "net/http"` + "\n")
|
|
handlerContent.WriteString(` "strconv"` + "\n")
|
|
handlerContent.WriteString(` "strings"` + "\n")
|
|
handlerContent.WriteString(` "sync"` + "\n")
|
|
handlerContent.WriteString(` "time"` + "\n\n")
|
|
handlerContent.WriteString(` "github.com/gin-gonic/gin"` + "\n")
|
|
handlerContent.WriteString(` "github.com/go-playground/validator/v10"` + "\n")
|
|
handlerContent.WriteString(` "github.com/google/uuid"` + "\n")
|
|
handlerContent.WriteString(")\n\n")
|
|
|
|
// Vars
|
|
handlerContent.WriteString("var (\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "db database.Service\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "once sync.Once\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "validate *validator.Validate\n")
|
|
handlerContent.WriteString(")\n\n")
|
|
|
|
// init func
|
|
handlerContent.WriteString("// Initialize the database connection and validator\n")
|
|
handlerContent.WriteString("func init() {\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "once.Do(func() {\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "db = database.New(config.LoadConfig())\n")
|
|
handlerContent.WriteString(" " + data.NameLower + "validate = validator.New()\n")
|
|
|
|
// Only register validation if POST is enabled
|
|
if data.HasPost {
|
|
handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n")
|
|
}
|
|
|
|
handlerContent.WriteString(" if " + data.NameLower + "db == nil {\n")
|
|
handlerContent.WriteString(" log.Fatal(\"Failed to initialize database connection\")\n")
|
|
handlerContent.WriteString(" }\n")
|
|
handlerContent.WriteString(" })\n")
|
|
handlerContent.WriteString("}\n\n")
|
|
|
|
// Custom validation - Only include if POST is enabled
|
|
if data.HasPost {
|
|
handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n")
|
|
handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n")
|
|
handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n")
|
|
handlerContent.WriteString("}\n\n")
|
|
}
|
|
|
|
// Handler struct
|
|
handlerContent.WriteString("// " + data.Name + "Handler handles " + data.NameLower + " services\n")
|
|
handlerContent.WriteString("type " + data.Name + "Handler struct {\n")
|
|
handlerContent.WriteString(" db database.Service\n")
|
|
handlerContent.WriteString("}\n\n")
|
|
|
|
// Constructor
|
|
handlerContent.WriteString("// New" + data.Name + "Handler creates a new " + data.Name + "Handler\n")
|
|
handlerContent.WriteString("func New" + data.Name + "Handler() *" + data.Name + "Handler {\n")
|
|
handlerContent.WriteString(" return &" + data.Name + "Handler{\n")
|
|
handlerContent.WriteString(" db: " + data.NameLower + "db,\n")
|
|
handlerContent.WriteString(" }\n")
|
|
handlerContent.WriteString("}\n")
|
|
|
|
// Add optional methods based on enabled flags
|
|
if data.HasGet {
|
|
handlerContent.WriteString(generateGetMethods(data))
|
|
}
|
|
if data.HasDynamic {
|
|
handlerContent.WriteString(generateDynamicMethod(data))
|
|
}
|
|
if data.HasSearch {
|
|
handlerContent.WriteString(generateSearchMethod(data))
|
|
}
|
|
if data.HasPost {
|
|
handlerContent.WriteString(generateCreateMethod(data))
|
|
}
|
|
if data.HasPut {
|
|
handlerContent.WriteString(generateUpdateMethod(data))
|
|
}
|
|
if data.HasDelete {
|
|
handlerContent.WriteString(generateDeleteMethod(data))
|
|
}
|
|
if data.HasStats {
|
|
handlerContent.WriteString(generateStatsMethod(data))
|
|
}
|
|
|
|
// Add helper methods - this function now handles conditional generation internally
|
|
handlerContent.WriteString(generateHelperMethods(data))
|
|
|
|
// Write into file
|
|
writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent.String())
|
|
}
|
|
|
|
func generateGetMethods(data HandlerData) string {
|
|
return `
|
|
|
|
// Get` + data.Name + ` godoc
|
|
// @Summary Get ` + data.NameLower + ` with pagination and optional aggregation
|
|
// @Description Returns a paginated list of ` + data.NamePlural + ` with optional summary statistics
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param limit query int false "Limit (max 100)" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Param include_summary query bool false "Include aggregation summary" default(false)
|
|
// @Param status query string false "Filter by status"
|
|
// @Param search query string false "Search in multiple fields"
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response"
|
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NamePlural + ` [get]
|
|
func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) {
|
|
// Parse pagination parameters
|
|
limit, offset, err := h.parsePaginationParams(c)
|
|
if err != nil {
|
|
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse filter parameters
|
|
filter := h.parseFilterParams(c)
|
|
includeAggregation := c.Query("include_summary") == "true"
|
|
|
|
// Get database connection
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Execute concurrent operations
|
|
var (
|
|
items []` + data.Category + `Models.` + data.Name + `
|
|
total int
|
|
aggregateData *models.AggregateData
|
|
wg sync.WaitGroup
|
|
errChan = make(chan error, 3)
|
|
mu sync.Mutex
|
|
)
|
|
|
|
// Fetch total count
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
|
|
mu.Lock()
|
|
errChan <- fmt.Errorf("failed to get total count: %w", err)
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Fetch main data
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset)
|
|
mu.Lock()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("failed to fetch data: %w", err)
|
|
} else {
|
|
items = result
|
|
}
|
|
mu.Unlock()
|
|
}()
|
|
|
|
// Fetch aggregation data if requested
|
|
if includeAggregation {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
result, err := h.getAggregateData(ctx, dbConn, filter)
|
|
mu.Lock()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
|
|
} else {
|
|
aggregateData = result
|
|
}
|
|
mu.Unlock()
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
wg.Wait()
|
|
close(errChan)
|
|
|
|
// Check for errors
|
|
for err := range errChan {
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Build response
|
|
meta := h.calculateMeta(limit, offset, total)
|
|
response := ` + data.Category + `Models.` + data.Name + `GetResponse{
|
|
Message: "Data ` + data.Category + ` berhasil diambil",
|
|
Data: items,
|
|
Meta: meta,
|
|
}
|
|
|
|
if includeAggregation && aggregateData != nil {
|
|
response.Summary = aggregateData
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// Get` + data.Name + `ByID godoc
|
|
// @Summary Get ` + data.Name + ` by ID
|
|
// @Description Returns a single ` + data.NameLower + ` by ID
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "` + data.Name + ` ID (UUID)"
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetByIDResponse "Success response"
|
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
|
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NameLower + `/{id} [get]
|
|
func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// Validate UUID format
|
|
if _, err := uuid.Parse(id); err != nil {
|
|
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
item, err := h.get` + data.Name + `ByID(ctx, dbConn, id)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
|
|
} else {
|
|
h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
response := ` + data.Category + `Models.` + data.Name + `GetByIDResponse{
|
|
Message: "` + data.Category + ` details retrieved successfully",
|
|
Data: item,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}`
|
|
}
|
|
|
|
func generateDynamicMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Get` + data.Name + `Dynamic godoc
|
|
// @Summary Get ` + data.NameLower + ` with dynamic filtering
|
|
// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param fields query string false "Fields to select (e.g., fields=*.*)"
|
|
// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[name][_eq]=value)"
|
|
// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)"
|
|
// @Param limit query int false "Limit" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response"
|
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NamePlural + `/dynamic [get]
|
|
func (h *` + data.Name + `Handler) Get` + data.Name + `Dynamic(c *gin.Context) {
|
|
// Parse query parameters
|
|
parser := utils.NewQueryParser().SetLimits(10, 100)
|
|
dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query())
|
|
if err != nil {
|
|
h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get database connection
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Execute query with dynamic filtering
|
|
items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery)
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Build response
|
|
meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total)
|
|
response := ` + data.Category + `Models.` + data.Name + `GetResponse{
|
|
Message: "Data ` + data.Category + ` berhasil diambil",
|
|
Data: items,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}`
|
|
}
|
|
|
|
func generateSearchMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Get` + data.Name + `Search godoc
|
|
// @Summary Get ` + data.NameLower + ` with Search filtering
|
|
// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param fields query string false "Fields to select (e.g., fields=*.*)"
|
|
// @Param filter[column][operator] query string false "Search filters (e.g., filter[name][_eq]=value)"
|
|
// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)"
|
|
// @Param limit query int false "Limit" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response"
|
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NamePlural + `/search [get]
|
|
func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) {
|
|
// Parse complex search parameters
|
|
searchQuery := c.Query("q")
|
|
if searchQuery == "" {
|
|
// If no search query provided, return all records with default sorting
|
|
query := utils.DynamicQuery{
|
|
Fields: []string{"*"},
|
|
Filters: []utils.FilterGroup{}, // Empty filters - fetch` + data.Name + `sDynamic will add default deleted filter
|
|
Sort: []utils.SortField{{
|
|
Column: "date_created",
|
|
Order: "DESC",
|
|
}},
|
|
Limit: 20,
|
|
Offset: 0,
|
|
}
|
|
|
|
// Parse pagination if provided
|
|
if limit := c.Query("limit"); limit != "" {
|
|
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
|
|
query.Limit = l
|
|
}
|
|
}
|
|
|
|
if offset := c.Query("offset"); offset != "" {
|
|
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
|
|
query.Offset = o
|
|
}
|
|
}
|
|
|
|
// Get database connection
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Execute query to get all records
|
|
` + data.NameLower + `s, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query)
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Build response
|
|
meta := h.calculateMeta(query.Limit, query.Offset, total)
|
|
response := ` + data.Category + `Models.` + data.Name + `GetResponse{
|
|
Message: "All records retrieved (no search query provided)",
|
|
Data: ` + data.NameLower + `s,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
return
|
|
}
|
|
|
|
// Build dynamic query for search
|
|
query := utils.DynamicQuery{
|
|
Fields: []string{"*"},
|
|
Filters: []utils.FilterGroup{{
|
|
Filters: []utils.DynamicFilter{
|
|
{
|
|
Column: "name",
|
|
Operator: utils.OpContains,
|
|
Value: searchQuery,
|
|
LogicOp: "OR",
|
|
},
|
|
},
|
|
LogicOp: "AND",
|
|
}},
|
|
Sort: []utils.SortField{{
|
|
Column: "date_created",
|
|
Order: "DESC",
|
|
}},
|
|
Limit: 20,
|
|
Offset: 0,
|
|
}
|
|
|
|
// Parse pagination if provided
|
|
if limit := c.Query("limit"); limit != "" {
|
|
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
|
|
query.Limit = l
|
|
}
|
|
}
|
|
|
|
if offset := c.Query("offset"); offset != "" {
|
|
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
|
|
query.Offset = o
|
|
}
|
|
}
|
|
|
|
// Get database connection
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Execute search
|
|
` + data.NameLower + `s, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query)
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Build response
|
|
meta := h.calculateMeta(query.Limit, query.Offset, total)
|
|
response := ` + data.Category + `Models.` + data.Name + `GetResponse{
|
|
Message: fmt.Sprintf("Search results for '%s'", searchQuery),
|
|
Data: ` + data.NameLower + `s,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
// fetch` + data.Name + `sDynamic executes dynamic query
|
|
func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) {
|
|
// Setup query builders
|
|
countBuilder := utils.NewQueryBuilder("` + data.TableName + `").
|
|
SetColumnMapping(map[string]string{
|
|
// Add your column mappings here
|
|
}).
|
|
SetAllowedColumns([]string{
|
|
"id", "status", "sort", "user_created", "date_created",
|
|
"user_updated", "date_updated", "name",
|
|
// Add other allowed columns here
|
|
})
|
|
|
|
mainBuilder := utils.NewQueryBuilder("` + data.TableName + `").
|
|
SetColumnMapping(map[string]string{
|
|
// Add your column mappings here
|
|
}).
|
|
SetAllowedColumns([]string{
|
|
"id", "status", "sort", "user_created", "date_created",
|
|
"user_updated", "date_updated", "name",
|
|
// Add other allowed columns here
|
|
})
|
|
|
|
// Add default filter to exclude deleted records
|
|
if len(query.Filters) > 0 {
|
|
query.Filters = append([]utils.FilterGroup{{
|
|
Filters: []utils.DynamicFilter{{
|
|
Column: "status",
|
|
Operator: utils.OpNotEqual,
|
|
Value: "deleted",
|
|
}},
|
|
LogicOp: "AND",
|
|
}}, query.Filters...)
|
|
} else {
|
|
query.Filters = []utils.FilterGroup{{
|
|
Filters: []utils.DynamicFilter{{
|
|
Column: "status",
|
|
Operator: utils.OpNotEqual,
|
|
Value: "deleted",
|
|
}},
|
|
LogicOp: "AND",
|
|
}}
|
|
}
|
|
|
|
// Execute queries sequentially
|
|
var total int
|
|
var items []` + data.Category + `Models.` + data.Name + `
|
|
|
|
// 1. Get total count
|
|
countQuery := query
|
|
countQuery.Limit = 0
|
|
countQuery.Offset = 0
|
|
|
|
countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to build count query: %w", err)
|
|
}
|
|
|
|
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get total count: %w", err)
|
|
}
|
|
|
|
// 2. Get main data
|
|
mainSQL, mainArgs, err := mainBuilder.BuildQuery(query)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to build main query: %w", err)
|
|
}
|
|
|
|
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to execute main query: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
item, err := h.scan` + data.Name + `(rows)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, fmt.Errorf("rows iteration error: %w", err)
|
|
}
|
|
|
|
return items, total, nil
|
|
}
|
|
`
|
|
}
|
|
|
|
func generateCreateMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Create` + data.Name + ` godoc
|
|
// @Summary Create ` + data.NameLower + `
|
|
// @Description Creates a new ` + data.NameLower + ` record
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body ` + data.Category + `Models.` + data.Name + `CreateRequest true "` + data.Name + ` creation request"
|
|
// @Success 201 {object} ` + data.Category + `Models.` + data.Name + `CreateResponse "` + data.Name + ` created successfully"
|
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NamePlural + ` [post]
|
|
func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) {
|
|
var req ` + data.Category + `Models.` + data.Name + `CreateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate request
|
|
if err := ` + data.NameLower + `validate.Struct(&req); err != nil {
|
|
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
// Validate duplicate and daily submission
|
|
if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil {
|
|
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
item, err := h.create` + data.Name + `(ctx, dbConn, &req)
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response := ` + data.Category + `Models.` + data.Name + `CreateResponse{
|
|
Message: "` + data.Name + ` berhasil dibuat",
|
|
Data: item,
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, response)
|
|
}`
|
|
}
|
|
|
|
func generateUpdateMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Update` + data.Name + ` godoc
|
|
// @Summary Update ` + data.NameLower + `
|
|
// @Description Updates an existing ` + data.NameLower + ` record
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "` + data.Name + ` ID (UUID)"
|
|
// @Param request body ` + data.Category + `Models.` + data.Name + `UpdateRequest true "` + data.Name + ` update request"
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully"
|
|
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
|
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NameLower + `/{id} [put]
|
|
func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// Validate UUID format
|
|
if _, err := uuid.Parse(id); err != nil {
|
|
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var req ` + data.Category + `Models.` + data.Name + `UpdateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Set ID from path parameter
|
|
req.ID = id
|
|
|
|
// Validate request
|
|
if err := ` + data.NameLower + `validate.Struct(&req); err != nil {
|
|
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
item, err := h.update` + data.Name + `(ctx, dbConn, &req)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
|
|
} else {
|
|
h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
response := ` + data.Category + `Models.` + data.Name + `UpdateResponse{
|
|
Message: "` + data.Name + ` berhasil diperbarui",
|
|
Data: item,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}`
|
|
}
|
|
|
|
func generateDeleteMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Delete` + data.Name + ` godoc
|
|
// @Summary Delete ` + data.NameLower + `
|
|
// @Description Soft deletes a ` + data.NameLower + ` by setting status to 'deleted'
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "` + data.Name + ` ID (UUID)"
|
|
// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully"
|
|
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
|
// @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NameLower + `/{id} [delete]
|
|
func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// Validate UUID format
|
|
if _, err := uuid.Parse(id); err != nil {
|
|
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
err = h.delete` + data.Name + `(ctx, dbConn, id)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound)
|
|
} else {
|
|
h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
response := ` + data.Category + `Models.` + data.Name + `DeleteResponse{
|
|
Message: "` + data.Name + ` berhasil dihapus",
|
|
ID: id,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}`
|
|
}
|
|
|
|
func generateStatsMethod(data HandlerData) string {
|
|
return `
|
|
|
|
// Get` + data.Name + `Stats godoc
|
|
// @Summary Get ` + data.NameLower + ` statistics
|
|
// @Description Returns comprehensive statistics about ` + data.NameLower + ` data
|
|
// @Tags ` + data.Name + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param status query string false "Filter statistics by status"
|
|
// @Success 200 {object} models.AggregateData "Statistics data"
|
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
|
// @Router /api/v1/` + data.NamePlural + `/stats [get]
|
|
func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) {
|
|
dbConn, err := h.db.GetDB("postgres_satudata")
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
filter := h.parseFilterParams(c)
|
|
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
|
|
if err != nil {
|
|
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Statistik ` + data.NameLower + ` berhasil diambil",
|
|
"data": aggregateData,
|
|
})
|
|
}`
|
|
}
|
|
|
|
func generateHelperMethods(data HandlerData) string {
|
|
var helperMethods strings.Builder
|
|
|
|
// Helper methods yang selalu dibutuhkan untuk semua handlers
|
|
helperMethods.WriteString(`
|
|
|
|
// Optimized scanning function - selalu dibutuhkan untuk semua operasi database
|
|
func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `Models.` + data.Name + `, error) {
|
|
var item ` + data.Category + `Models.` + data.Name + `
|
|
|
|
// Scan into individual fields to handle nullable types properly
|
|
err := rows.Scan(
|
|
&item.ID,
|
|
&item.Status,
|
|
&item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32
|
|
&item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString
|
|
&item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime
|
|
&item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString
|
|
&item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime
|
|
&item.Name.String, &item.Name.Valid, // sql.NullString
|
|
)
|
|
|
|
return item, err
|
|
}
|
|
|
|
// Enhanced error handling - selalu dibutuhkan untuk semua handlers
|
|
func (h *` + data.Name + `Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
|
log.Printf("[ERROR] %s: %v", message, err)
|
|
h.respondError(c, message, err, statusCode)
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, err error, statusCode int) {
|
|
errorMessage := message
|
|
if gin.Mode() == gin.ReleaseMode {
|
|
errorMessage = "Internal server error"
|
|
}
|
|
|
|
c.JSON(statusCode, models.ErrorResponse{
|
|
Error: errorMessage,
|
|
Code: statusCode,
|
|
Message: err.Error(),
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
`)
|
|
|
|
// Helper methods untuk GET operations
|
|
if data.HasGet {
|
|
helperMethods.WriteString(`
|
|
|
|
// Database operations untuk GET by ID
|
|
func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `Models.` + data.Name + `, error) {
|
|
query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'"
|
|
row := dbConn.QueryRowContext(ctx, query, id)
|
|
|
|
var item ` + data.Category + `Models.` + data.Name + `
|
|
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &item, nil
|
|
}
|
|
`)
|
|
|
|
// Helper untuk fetch dengan pagination/filter
|
|
helperMethods.WriteString(`
|
|
|
|
func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `Models.` + data.Name + `, error) {
|
|
whereClause, args := h.buildWhereClause(filter)
|
|
query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := dbConn.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]` + data.Category + `Models.` + data.Name + `, 0, limit)
|
|
for rows.Next() {
|
|
item, err := h.scan` + data.Name + `(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("rows iteration error: %w", err)
|
|
}
|
|
|
|
if *verboseFlag {
|
|
log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items))
|
|
}
|
|
return items, nil
|
|
}
|
|
`)
|
|
|
|
// Helper untuk pagination
|
|
helperMethods.WriteString(`
|
|
|
|
// Parse pagination parameters dengan validation yang lebih ketat
|
|
func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) {
|
|
limit := 10 // Default limit
|
|
offset := 0 // Default offset
|
|
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
parsedLimit, err := strconv.Atoi(limitStr)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
|
|
}
|
|
if parsedLimit <= 0 {
|
|
return 0, 0, fmt.Errorf("limit must be greater than 0")
|
|
}
|
|
if parsedLimit > 100 {
|
|
return 0, 0, fmt.Errorf("limit cannot exceed 100")
|
|
}
|
|
limit = parsedLimit
|
|
}
|
|
|
|
if offsetStr := c.Query("offset"); offsetStr != "" {
|
|
parsedOffset, err := strconv.Atoi(offsetStr)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
|
|
}
|
|
if parsedOffset < 0 {
|
|
return 0, 0, fmt.Errorf("offset cannot be negative")
|
|
}
|
|
offset = parsedOffset
|
|
}
|
|
|
|
if *verboseFlag {
|
|
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
|
|
}
|
|
return limit, offset, nil
|
|
}
|
|
`)
|
|
|
|
// Helper untuk filter (jika ada filter atau search)
|
|
if data.HasFilter || data.HasSearch {
|
|
helperMethods.WriteString(`
|
|
|
|
func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `Models.` + data.Name + `Filter {
|
|
filter := ` + data.Category + `Models.` + data.Name + `Filter{}
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
if models.IsValidStatus(status) {
|
|
filter.Status = &status
|
|
}
|
|
}
|
|
|
|
if search := c.Query("search"); search != "" {
|
|
filter.Search = &search
|
|
}
|
|
|
|
// Parse date filters
|
|
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
|
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
|
filter.DateFrom = &dateFrom
|
|
}
|
|
}
|
|
|
|
if dateToStr := c.Query("date_to"); dateToStr != "" {
|
|
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
|
filter.DateTo = &dateTo
|
|
}
|
|
}
|
|
|
|
return filter
|
|
}
|
|
|
|
// Build WHERE clause dengan filter parameters
|
|
func (h *` + data.Name + `Handler) buildWhereClause(filter ` + data.Category + `Models.` + data.Name + `Filter) (string, []interface{}) {
|
|
conditions := []string{"status != 'deleted'"}
|
|
args := []interface{}{}
|
|
paramCount := 1
|
|
|
|
if filter.Status != nil {
|
|
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
|
|
args = append(args, *filter.Status)
|
|
paramCount++
|
|
}
|
|
|
|
if filter.Search != nil {
|
|
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
|
|
conditions = append(conditions, searchCondition)
|
|
searchTerm := "%" + *filter.Search + "%"
|
|
args = append(args, searchTerm)
|
|
paramCount++
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
|
|
args = append(args, *filter.DateFrom)
|
|
paramCount++
|
|
}
|
|
|
|
if filter.DateTo != nil {
|
|
conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
|
|
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
|
|
paramCount++
|
|
}
|
|
|
|
return strings.Join(conditions, " AND "), args
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Helper untuk pagination meta
|
|
helperMethods.WriteString(`
|
|
|
|
func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
|
totalPages := 0
|
|
currentPage := 1
|
|
if limit > 0 {
|
|
totalPages = (total + limit - 1) / limit // Ceiling division
|
|
currentPage = (offset / limit) + 1
|
|
}
|
|
|
|
return models.MetaResponse{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
Total: total,
|
|
TotalPages: totalPages,
|
|
CurrentPage: currentPage,
|
|
HasNext: offset+limit < total,
|
|
HasPrev: offset > 0,
|
|
}
|
|
}
|
|
`)
|
|
|
|
// Helper untuk total count (dibutuhkan untuk pagination dan stats)
|
|
if data.HasPagination || data.HasStats {
|
|
helperMethods.WriteString(`
|
|
|
|
func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter, total *int) error {
|
|
whereClause, args := h.buildWhereClause(filter)
|
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause)
|
|
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
|
|
return fmt.Errorf("total count query failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Helper untuk aggregate data (stats)
|
|
if data.HasStats {
|
|
helperMethods.WriteString(`
|
|
|
|
// Get comprehensive aggregate data dengan filter support
|
|
func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter) (*models.AggregateData, error) {
|
|
aggregate := &models.AggregateData{
|
|
ByStatus: make(map[string]int),
|
|
}
|
|
|
|
// Build where clause untuk filter
|
|
whereClause, args := h.buildWhereClause(filter)
|
|
|
|
// Use concurrent execution untuk performance
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
errChan := make(chan error, 4)
|
|
|
|
// 1. Count by status
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause)
|
|
|
|
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("status query failed: %w", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
mu.Lock()
|
|
for rows.Next() {
|
|
var status string
|
|
var count int
|
|
if err := rows.Scan(&status, &count); err != nil {
|
|
mu.Unlock()
|
|
errChan <- fmt.Errorf("status scan failed: %w", err)
|
|
return
|
|
}
|
|
aggregate.ByStatus[status] = count
|
|
switch status {
|
|
case "active":
|
|
aggregate.TotalActive = count
|
|
case "draft":
|
|
aggregate.TotalDraft = count
|
|
case "inactive":
|
|
aggregate.TotalInactive = count
|
|
}
|
|
}
|
|
mu.Unlock()
|
|
|
|
if err := rows.Err(); err != nil {
|
|
errChan <- fmt.Errorf("status iteration error: %w", err)
|
|
}
|
|
}()
|
|
|
|
// 2. Get last updated time dan today statistics
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// Last updated
|
|
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause)
|
|
var lastUpdated sql.NullTime
|
|
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
|
|
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
// Today statistics
|
|
today := time.Now().Format("2006-01-02")
|
|
todayStatsQuery := fmt.Sprintf(` + "`" + `
|
|
SELECT
|
|
SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today,
|
|
SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today
|
|
FROM ` + data.TableName + `
|
|
WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause)
|
|
|
|
todayArgs := append(args, today)
|
|
var createdToday, updatedToday int
|
|
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
|
|
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
if lastUpdated.Valid {
|
|
aggregate.LastUpdated = &lastUpdated.Time
|
|
}
|
|
aggregate.CreatedToday = createdToday
|
|
aggregate.UpdatedToday = updatedToday
|
|
mu.Unlock()
|
|
}()
|
|
|
|
// Wait for all goroutines
|
|
wg.Wait()
|
|
close(errChan)
|
|
|
|
// Check for errors
|
|
for err := range errChan {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return aggregate, nil
|
|
}
|
|
`)
|
|
}
|
|
}
|
|
|
|
// Helper methods untuk POST operations
|
|
if data.HasPost {
|
|
helperMethods.WriteString(`
|
|
|
|
// Database operations untuk CREATE
|
|
func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) (*` + data.Category + `Models.` + data.Name + `, error) {
|
|
id := uuid.New().String()
|
|
now := time.Now()
|
|
|
|
query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
|
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
|
|
|
|
var item ` + data.Category + `Models.` + data.Name + `
|
|
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err)
|
|
}
|
|
|
|
return &item, nil
|
|
}
|
|
|
|
// validate` + data.Name + `Submission performs validation for duplicate entries and daily submission limits
|
|
func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) error {
|
|
// Import the validation utility
|
|
validator := validation.NewDuplicateValidator(dbConn)
|
|
|
|
// Use default configuration
|
|
config := validation.ValidationConfig{
|
|
TableName: "` + data.TableName + `",
|
|
IDColumn: "id",
|
|
StatusColumn: "status",
|
|
DateColumn: "date_created",
|
|
ActiveStatuses: []string{"active", "draft"},
|
|
}
|
|
|
|
// Validate duplicate entries with active status for today
|
|
err := validator.ValidateDuplicate(ctx, config, "dummy_id")
|
|
if err != nil {
|
|
return fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
// Validate once per day submission
|
|
err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit")
|
|
if err != nil {
|
|
return fmt.Errorf("daily submission limit exceeded: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Example usage of the validation utility with custom configuration
|
|
func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) error {
|
|
// Create validator instance
|
|
validator := validation.NewDuplicateValidator(dbConn)
|
|
|
|
// Use custom configuration
|
|
config := validation.ValidationConfig{
|
|
TableName: "` + data.TableName + `",
|
|
IDColumn: "id",
|
|
StatusColumn: "status",
|
|
DateColumn: "date_created",
|
|
ActiveStatuses: []string{"active", "draft"},
|
|
AdditionalFields: map[string]interface{}{
|
|
"name": req.Name,
|
|
},
|
|
}
|
|
|
|
// Validate with custom fields
|
|
fields := map[string]interface{}{
|
|
"name": *req.Name,
|
|
}
|
|
|
|
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
|
if err != nil {
|
|
return fmt.Errorf("custom validation failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetLastSubmissionTime example
|
|
func (h *` + data.Name + `Handler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
|
|
validator := validation.NewDuplicateValidator(dbConn)
|
|
return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier)
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Helper methods untuk PUT operations
|
|
if data.HasPut {
|
|
helperMethods.WriteString(`
|
|
|
|
// Database operations untuk UPDATE
|
|
func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `UpdateRequest) (*` + data.Category + `Models.` + data.Name + `, error) {
|
|
now := time.Now()
|
|
|
|
query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
|
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
|
|
|
|
var item ` + data.Category + `Models.` + data.Name + `
|
|
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err)
|
|
}
|
|
|
|
return &item, nil
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Helper methods untuk DELETE operations
|
|
if data.HasDelete {
|
|
helperMethods.WriteString(`
|
|
|
|
// Database operations untuk DELETE
|
|
func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error {
|
|
now := time.Now()
|
|
query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'"
|
|
|
|
result, err := dbConn.ExecContext(ctx, query, id, now)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get affected rows: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
|
|
return nil
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Helper methods untuk DYNAMIC operations
|
|
if data.HasDynamic {
|
|
helperMethods.WriteString(`
|
|
// fetch` + data.Name + `sDynamic executes dynamic query
|
|
func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) {
|
|
// Setup query builders
|
|
countBuilder := utils.NewQueryBuilder("` + data.TableName + `").
|
|
SetColumnMapping(map[string]string{
|
|
// Add your column mappings here
|
|
}).
|
|
SetAllowedColumns([]string{
|
|
"id", "status", "sort", "user_created", "date_created",
|
|
"user_updated", "date_updated", "name",
|
|
// Add other allowed columns here
|
|
})
|
|
|
|
mainBuilder := utils.NewQueryBuilder("` + data.TableName + `").
|
|
SetColumnMapping(map[string]string{
|
|
// Add your column mappings here
|
|
}).
|
|
SetAllowedColumns([]string{
|
|
"id", "status", "sort", "user_created", "date_created",
|
|
"user_updated", "date_updated", "name",
|
|
// Add other allowed columns here
|
|
})
|
|
|
|
// Add default filter to exclude deleted records
|
|
if len(query.Filters) > 0 {
|
|
query.Filters = append([]utils.FilterGroup{{
|
|
Filters: []utils.DynamicFilter{{
|
|
Column: "status",
|
|
Operator: utils.OpNotEqual,
|
|
Value: "deleted",
|
|
}},
|
|
LogicOp: "AND",
|
|
}}, query.Filters...)
|
|
} else {
|
|
query.Filters = []utils.FilterGroup{{
|
|
Filters: []utils.DynamicFilter{{
|
|
Column: "status",
|
|
Operator: utils.OpNotEqual,
|
|
Value: "deleted",
|
|
}},
|
|
LogicOp: "AND",
|
|
}}
|
|
}
|
|
|
|
// Execute queries sequentially
|
|
var total int
|
|
var items []` + data.Category + `Models.` + data.Name + `
|
|
|
|
// 1. Get total count
|
|
countQuery := query
|
|
countQuery.Limit = 0
|
|
countQuery.Offset = 0
|
|
|
|
countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to build count query: %w", err)
|
|
}
|
|
|
|
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get total count: %w", err)
|
|
}
|
|
|
|
// 2. Get main data
|
|
mainSQL, mainArgs, err := mainBuilder.BuildQuery(query)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to build main query: %w", err)
|
|
}
|
|
|
|
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to execute main query: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
item, err := h.scan` + data.Name + `(rows)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err)
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, fmt.Errorf("rows iteration error: %w", err)
|
|
}
|
|
|
|
return items, total, nil
|
|
}
|
|
`)
|
|
}
|
|
|
|
return helperMethods.String()
|
|
}
|
|
|
|
// ================= MODEL GENERATION =====================
|
|
func generateModelFile(data HandlerData, modelDir string) {
|
|
// Tentukan nama file model
|
|
modelFileName := data.NameLower + ".go"
|
|
modelFilePath := filepath.Join(modelDir, modelFileName)
|
|
|
|
// Periksa apakah file model sudah ada
|
|
if _, err := os.Stat(modelFilePath); err == nil {
|
|
// File sudah ada, skip pembuatan model
|
|
fmt.Printf("Model %s already exists, skipping generation\n", data.Name)
|
|
return
|
|
}
|
|
|
|
// Tentukan import block
|
|
var importBlock, nullablePrefix string
|
|
|
|
if data.Category == "models" {
|
|
importBlock = `import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
`
|
|
} else {
|
|
nullablePrefix = "models."
|
|
importBlock = `import (
|
|
"` + data.ModuleName + `/internal/models"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
`
|
|
}
|
|
|
|
modelContent := `package ` + data.Category + `
|
|
|
|
` + importBlock + `
|
|
|
|
// ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table
|
|
// with proper null handling and optimized JSON marshaling
|
|
type ` + data.Name + ` struct {
|
|
ID string ` + "`json:\"id\" db:\"id\"`" + `
|
|
Status string ` + "`json:\"status\" db:\"status\"`" + `
|
|
Sort ` + nullablePrefix + "NullableInt32 `json:\"sort,omitempty\" db:\"sort\"`" + `
|
|
UserCreated sql.NullString ` + "`json:\"user_created,omitempty\" db:\"user_created\"`" + `
|
|
DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + `
|
|
UserUpdated sql.NullString ` + "`json:\"user_updated,omitempty\" db:\"user_updated\"`" + `
|
|
DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + `
|
|
Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + `
|
|
}
|
|
|
|
// Custom JSON marshaling untuk ` + data.Name + ` agar NULL values tidak muncul di response
|
|
func (r ` + data.Name + `) MarshalJSON() ([]byte, error) {
|
|
type Alias ` + data.Name + `
|
|
aux := &struct {
|
|
Sort *int ` + "`json:\"sort,omitempty\"`" + `
|
|
UserCreated *string ` + "`json:\"user_created,omitempty\"`" + `
|
|
DateCreated *time.Time ` + "`json:\"date_created,omitempty\"`" + `
|
|
UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + `
|
|
DateUpdated *time.Time ` + "`json:\"date_updated,omitempty\"`" + `
|
|
Name *string ` + "`json:\"name,omitempty\"`" + `
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(&r),
|
|
}
|
|
|
|
if r.Sort.Valid {
|
|
sort := int(r.Sort.Int32)
|
|
aux.Sort = &sort
|
|
}
|
|
if r.UserCreated.Valid {
|
|
aux.UserCreated = &r.UserCreated.String
|
|
}
|
|
if r.DateCreated.Valid {
|
|
aux.DateCreated = &r.DateCreated.Time
|
|
}
|
|
if r.UserUpdated.Valid {
|
|
aux.UserUpdated = &r.UserUpdated.String
|
|
}
|
|
if r.DateUpdated.Valid {
|
|
aux.DateUpdated = &r.DateUpdated.Time
|
|
}
|
|
if r.Name.Valid {
|
|
aux.Name = &r.Name.String
|
|
}
|
|
return json.Marshal(aux)
|
|
}
|
|
|
|
// Helper methods untuk mendapatkan nilai yang aman
|
|
func (r *` + data.Name + `) GetName() string {
|
|
if r.Name.Valid {
|
|
return r.Name.String
|
|
}
|
|
return ""
|
|
}
|
|
`
|
|
|
|
// Add request/response structs based on enabled methods
|
|
if data.HasGet {
|
|
modelContent += `
|
|
|
|
// Response struct untuk GET by ID
|
|
type ` + data.Name + `GetByIDResponse struct {
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
|
|
}
|
|
|
|
// Enhanced GET response dengan pagination dan aggregation
|
|
type ` + data.Name + `GetResponse struct {
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Data []` + data.Name + ` ` + "`json:\"data\"`" + `
|
|
Meta ` + nullablePrefix + "MetaResponse `json:\"meta\"`" + `
|
|
Summary *` + nullablePrefix + "AggregateData `json:\"summary,omitempty\"`" + `
|
|
}
|
|
`
|
|
}
|
|
if data.HasPost {
|
|
modelContent += `
|
|
|
|
// Request struct untuk create
|
|
type ` + data.Name + `CreateRequest struct {
|
|
Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + `
|
|
Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + `
|
|
}
|
|
|
|
// Response struct untuk create
|
|
type ` + data.Name + `CreateResponse struct {
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
|
|
}
|
|
`
|
|
}
|
|
if data.HasPut {
|
|
modelContent += `
|
|
|
|
// Update request
|
|
type ` + data.Name + `UpdateRequest struct {
|
|
ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + `
|
|
Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + `
|
|
Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + `
|
|
}
|
|
|
|
// Response struct untuk update
|
|
type ` + data.Name + `UpdateResponse struct {
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
|
|
}
|
|
`
|
|
}
|
|
if data.HasDelete {
|
|
modelContent += `
|
|
|
|
// Response struct untuk delete
|
|
type ` + data.Name + `DeleteResponse struct {
|
|
Message string ` + "`json:\"message\"`" + `
|
|
ID string ` + "`json:\"id\"`" + `
|
|
}
|
|
`
|
|
}
|
|
// Add filter struct
|
|
modelContent += `
|
|
|
|
// Filter struct untuk query parameters
|
|
type ` + data.Name + `Filter struct {
|
|
Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + `
|
|
Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + `
|
|
DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + `
|
|
DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + `
|
|
}
|
|
`
|
|
writeFile(modelFilePath, modelContent)
|
|
fmt.Printf("Successfully generated model: %s\n", modelFileName)
|
|
}
|
|
|
|
// ================= ROUTES GENERATION =====================
|
|
func updateRoutesFile(data HandlerData) {
|
|
routesFile := "../../internal/routes/v1/routes.go"
|
|
content, err := os.ReadFile(routesFile)
|
|
if err != nil {
|
|
fmt.Printf("⚠️ Could not read routes.go: %v\n", err)
|
|
fmt.Printf("📝 Please manually add these routes to your routes.go file:\n")
|
|
printRoutesSample(data)
|
|
return
|
|
}
|
|
|
|
routesContent := string(content)
|
|
|
|
// Clean up duplicate routes first
|
|
routesContent = cleanupDuplicateRoutes(routesContent, data)
|
|
|
|
// Build import path
|
|
var importPath, importAlias string
|
|
if data.Category != "models" {
|
|
importPath = fmt.Sprintf("%s/internal/handlers/"+data.Category, data.ModuleName)
|
|
importAlias = data.Category + data.Name + "Handlers"
|
|
} else {
|
|
importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName)
|
|
importAlias = data.NameLower + "Handlers"
|
|
}
|
|
|
|
// Add import
|
|
importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath)
|
|
if !strings.Contains(routesContent, importPattern) {
|
|
importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath)
|
|
if strings.Contains(routesContent, "import (") {
|
|
routesContent = strings.Replace(routesContent, "import (",
|
|
"import (\n"+importToAdd, 1)
|
|
}
|
|
}
|
|
|
|
// Check if routes for this specific endpoint already exist
|
|
if routeBlockExists(routesContent, data) {
|
|
if *verboseFlag {
|
|
fmt.Printf("✅ Routes for %s (%s) already exist, skipping...\n", data.Name, getGroupPath(data))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Build new routes in protected group format
|
|
newRoutes := generateProtectedRouteBlock(data)
|
|
|
|
// Use the correct marker for insertion
|
|
insertMarker := ` // =============================================================================
|
|
// PUBLISHED ROUTES
|
|
// =============================================================================
|
|
`
|
|
|
|
// Find the position to insert routes
|
|
if strings.Contains(routesContent, insertMarker) {
|
|
// Insert after the marker block (including the newlines)
|
|
markerIndex := strings.Index(routesContent, insertMarker)
|
|
if markerIndex != -1 {
|
|
// Find the end of the marker block (including the newlines)
|
|
endOfMarker := markerIndex + len(insertMarker)
|
|
|
|
// Insert the new routes after the marker
|
|
routesContent = routesContent[:endOfMarker] + "\n" + newRoutes + routesContent[endOfMarker:]
|
|
|
|
if *verboseFlag {
|
|
fmt.Printf("📍 Inserted routes after PUBLISHED ROUTES marker\n")
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback: try to find alternative markers
|
|
alternativeMarkers := []string{
|
|
"// ============= PUBLISHED ROUTES ===============================================",
|
|
"// PUBLISHED ROUTES",
|
|
"return r", // End of function
|
|
}
|
|
|
|
inserted := false
|
|
for _, marker := range alternativeMarkers {
|
|
if strings.Contains(routesContent, marker) {
|
|
if marker == "return r" {
|
|
// Insert before the return statement
|
|
routesContent = strings.Replace(routesContent, "\t"+marker,
|
|
newRoutes+"\n\n\t"+marker, 1)
|
|
} else {
|
|
// Insert after the marker
|
|
routesContent = strings.Replace(routesContent, marker,
|
|
marker+"\n"+newRoutes, 1)
|
|
}
|
|
if *verboseFlag {
|
|
fmt.Printf("📍 Inserted routes using alternative marker: %s\n", marker)
|
|
}
|
|
inserted = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !inserted {
|
|
// Last resort: append at the end of the file
|
|
routesContent += "\n" + newRoutes
|
|
if *verboseFlag {
|
|
fmt.Printf("📍 Appended routes at the end of file\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil {
|
|
logError("Error writing routes.go", err, *verboseFlag)
|
|
return
|
|
}
|
|
|
|
if *verboseFlag {
|
|
fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name)
|
|
}
|
|
}
|
|
|
|
// routeBlockExists checks if a route block for the specific endpoint already exists
|
|
func routeBlockExists(content string, data HandlerData) bool {
|
|
groupPath := getGroupPath(data)
|
|
handlerName := getHandlerName(data)
|
|
|
|
// Build the expected route block pattern - lebih spesifik
|
|
patterns := []string{
|
|
// Pattern 1: Full route block dengan comment (paling spesifik)
|
|
fmt.Sprintf("// %s endpoints\n %sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")",
|
|
data.Name, handlerName, handlerName, data.Name, handlerName, groupPath),
|
|
|
|
// Pattern 2: Handler dan Group declaration (tanpa comment)
|
|
fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")",
|
|
handlerName, handlerName, data.Name, handlerName, groupPath),
|
|
|
|
// Pattern 3: Group declaration saja (jika handler sudah ada di import)
|
|
fmt.Sprintf("%sGroup := v1.Group(\"/%s\")", handlerName, groupPath),
|
|
|
|
// Pattern 4: Cari kombinasi handler dan group dengan spasi yang berbeda
|
|
fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name),
|
|
}
|
|
|
|
// Check if any of the patterns exist
|
|
for _, pattern := range patterns {
|
|
if strings.Contains(content, pattern) {
|
|
if *verboseFlag {
|
|
fmt.Printf("🔍 Found existing route pattern: %s\n", pattern)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Additional check: look for any route with the same group path
|
|
groupPattern := fmt.Sprintf("v1.Group(\"/%s\")", groupPath)
|
|
if strings.Contains(content, groupPattern) {
|
|
if *verboseFlag {
|
|
fmt.Printf("🔍 Found existing group pattern: %s\n", groupPattern)
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getGroupPath returns the group path for the given data
|
|
func getGroupPath(data HandlerData) string {
|
|
if data.Category != "models" {
|
|
return strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name)
|
|
}
|
|
return strings.ToLower(data.Name)
|
|
}
|
|
|
|
// getHandlerName returns the handler name for the given data
|
|
func getHandlerName(data HandlerData) string {
|
|
if data.Category != "models" {
|
|
return data.Category + data.Name
|
|
}
|
|
return strings.ToLower(data.Name)
|
|
}
|
|
|
|
func generateProtectedRouteBlock(data HandlerData) string {
|
|
var sb strings.Builder
|
|
handlerName := getHandlerName(data)
|
|
groupPath := getGroupPath(data)
|
|
|
|
// Komentar dan deklarasi handler & grup
|
|
sb.WriteString("// ")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString(" endpoints\n")
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler := ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handlers.New")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString("Handler()\n ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group := v1.Group(\"/")
|
|
sb.WriteString(groupPath)
|
|
sb.WriteString("\")\n {\n ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.GET(\"\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Get")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString(")\n")
|
|
|
|
if data.HasDynamic {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.GET(\"/dynamic\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Get")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString("Dynamic) // Route baru\n")
|
|
}
|
|
if data.HasSearch {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.GET(\"/search\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Search")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString("Advanced) // Route pencarian\n")
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.GET(\"/:id\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Get")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString("ByID)\n")
|
|
|
|
if data.HasPost {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.POST(\"\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Create")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString(")\n")
|
|
}
|
|
if data.HasPut {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.PUT(\"/:id\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Update")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString(")\n")
|
|
}
|
|
if data.HasDelete {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.DELETE(\"/:id\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Delete")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString(")\n")
|
|
}
|
|
if data.HasStats {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Group.GET(\"/stats\", ")
|
|
sb.WriteString(handlerName)
|
|
sb.WriteString("Handler.Get")
|
|
sb.WriteString(data.Name)
|
|
sb.WriteString("Stats)\n")
|
|
}
|
|
sb.WriteString(" }\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func cleanupDuplicateRoutes(content string, data HandlerData) string {
|
|
// Implement getGroupPath logic directly
|
|
var groupPath string
|
|
if data.Category != "models" {
|
|
groupPath = strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name)
|
|
} else {
|
|
groupPath = strings.ToLower(data.Name)
|
|
}
|
|
|
|
// Implement getHandlerName logic directly
|
|
var handlerName string
|
|
if data.Category != "models" {
|
|
handlerName = data.Category + data.Name
|
|
} else {
|
|
handlerName = strings.ToLower(data.Name)
|
|
}
|
|
|
|
// Split content by lines for better processing
|
|
lines := strings.Split(content, "\n")
|
|
|
|
var cleanedLines []string
|
|
inRouteBlock := false
|
|
routeBlockFound := false
|
|
blockStartLine := -1
|
|
|
|
for i, line := range lines {
|
|
// Check if this line starts a route block for our endpoint
|
|
// Look for comment pattern first
|
|
if strings.Contains(line, fmt.Sprintf("// %s endpoints", data.Name)) {
|
|
if routeBlockFound {
|
|
// This is a duplicate, skip it
|
|
if *verboseFlag {
|
|
fmt.Printf("🧹 Skipping duplicate route block for %s at line %d\n", data.Name, i+1)
|
|
}
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
continue
|
|
} else {
|
|
// This is the first occurrence, keep it
|
|
routeBlockFound = true
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
cleanedLines = append(cleanedLines, line)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Alternative pattern: look for handler declaration without comment
|
|
if !inRouteBlock && strings.Contains(line, fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name)) {
|
|
if routeBlockFound {
|
|
// This is a duplicate, skip it
|
|
if *verboseFlag {
|
|
fmt.Printf("🧹 Skipping duplicate handler declaration for %s at line %d\n", data.Name, i+1)
|
|
}
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
continue
|
|
} else {
|
|
// This is the first occurrence, keep it
|
|
routeBlockFound = true
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
cleanedLines = append(cleanedLines, line)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Third pattern: look for group declaration (for cases without handler declaration on same line)
|
|
if !inRouteBlock && strings.Contains(line, fmt.Sprintf("Group := v1.Group(\"/%s\")", groupPath)) {
|
|
if routeBlockFound {
|
|
// This is a duplicate, skip it
|
|
if *verboseFlag {
|
|
fmt.Printf("🧹 Skipping duplicate group declaration for %s at line %d\n", data.Name, i+1)
|
|
}
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
continue
|
|
} else {
|
|
// This is the first occurrence, keep it
|
|
routeBlockFound = true
|
|
inRouteBlock = true
|
|
blockStartLine = i
|
|
cleanedLines = append(cleanedLines, line)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If we're in a route block, check if we've reached the end
|
|
if inRouteBlock {
|
|
// Count braces to determine if we're at the end of the block
|
|
openBraces := strings.Count(line, "{")
|
|
closeBraces := strings.Count(line, "}")
|
|
|
|
// If we have more closing braces than opening braces, we might be at the end
|
|
if closeBraces > openBraces {
|
|
// Check if this line contains the closing brace for the route group
|
|
if strings.Contains(line, "}") && (strings.Contains(line, "Group") || strings.Contains(line, "handler")) {
|
|
// This is likely the end of our route block
|
|
if routeBlockFound && blockStartLine != -1 {
|
|
cleanedLines = append(cleanedLines, line)
|
|
inRouteBlock = false
|
|
blockStartLine = -1
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Additional check: look for standalone closing brace that might end the block
|
|
if strings.TrimSpace(line) == "}" && inRouteBlock {
|
|
// This might be the end of our route block
|
|
if routeBlockFound && blockStartLine != -1 {
|
|
cleanedLines = append(cleanedLines, line)
|
|
inRouteBlock = false
|
|
blockStartLine = -1
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Add the line if we're keeping this block
|
|
if routeBlockFound {
|
|
cleanedLines = append(cleanedLines, line)
|
|
}
|
|
} else {
|
|
// Not in a route block, just add the line
|
|
cleanedLines = append(cleanedLines, line)
|
|
}
|
|
}
|
|
|
|
return strings.Join(cleanedLines, "\n")
|
|
}
|
|
func printRoutesSample(data HandlerData) {
|
|
fmt.Print(generateProtectedRouteBlock(data))
|
|
fmt.Println()
|
|
}
|
|
|
|
// ================= UTILITY FUNCTIONS =====================
|
|
func writeFile(filename, content string) {
|
|
if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
|
|
logError(fmt.Sprintf("Error creating file %s", filename), err, *verboseFlag)
|
|
return
|
|
}
|
|
if *verboseFlag {
|
|
fmt.Printf("✅ Generated: %s\n", filename)
|
|
}
|
|
}
|
|
|
|
// Enhanced error logging function
|
|
func logError(message string, err error, verbose bool) {
|
|
if verbose {
|
|
log.Printf("❌ ERROR: %s - %v", message, err)
|
|
} else {
|
|
log.Printf("❌ ERROR: %s", message)
|
|
}
|
|
}
|
|
|
|
// Success logging function
|
|
func logSuccess(message string, details ...string) {
|
|
fmt.Printf("✅ %s", message)
|
|
for _, detail := range details {
|
|
fmt.Printf(" - %s", detail)
|
|
}
|
|
fmt.Println()
|
|
}
|