From 21cc6aacdb7d47976289c4fb1b5330c02b93f22c Mon Sep 17 00:00:00 2001 From: Meninjar Mulyono Date: Thu, 16 Oct 2025 08:40:04 +0700 Subject: [PATCH] Update Toll Generate --- README.md | 4 + internal/routes/v1/routes.go | 33 +- tools/bpjs/generate-handler.go | 3651 ++++++++++++++++++++++ tools/bpjs/services-config.yaml | 166 + tools/general/generate-handler.go | 2746 ++++++++++------ tools/general/old/generate-handlerold.go | 2000 ++++++++++++ tools/general/services-config.yaml | 250 ++ 7 files changed, 7903 insertions(+), 947 deletions(-) create mode 100644 tools/bpjs/generate-handler.go create mode 100644 tools/bpjs/services-config.yaml create mode 100644 tools/general/old/generate-handlerold.go create mode 100644 tools/general/services-config.yaml diff --git a/README.md b/README.md index 3bff8fd..9c2a30d 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,10 @@ go run tools/general/generate-handler.go retribusi get post put delete # Generate dengan fitur advanced go run tools/general/generate-handler.go retribusi get post put delete dynamic search stats + +# Config +go run tools/general/generate-handler.go --config tools/general/services-config.yaml --verbose + ``` *** diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 1e4632d..87c5569 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -97,7 +97,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { // ============================================================================= // PUBLISHED ROUTES - // ============================================================================= // Retribusi endpoints with retribusiHandler := retribusiHandlers.NewRetribusiHandler() @@ -128,24 +127,24 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { protected.Use(middleware.ConfigurableAuthMiddleware(cfg)) // Protected retribusi endpoints (Authentication Required) - protectedRetribusiGroup := protected.Group("/retribusi") - { - protectedRetribusiGroup.GET("", retribusiHandler.GetRetribusi) - protectedRetribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) - protectedRetribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) - protectedRetribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID) - protectedRetribusiGroup.POST("", func(c *gin.Context) { - retribusiHandler.CreateRetribusi(c) - }) + // protectedRetribusiGroup := protected.Group("/retribusi") + // { + // protectedRetribusiGroup.GET("", retribusiHandler.GetRetribusi) + // protectedRetribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) + // protectedRetribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) + // protectedRetribusiGroup.GET("/id/:id", retribusiHandler.GetRetribusiByID) + // protectedRetribusiGroup.POST("", func(c *gin.Context) { + // retribusiHandler.CreateRetribusi(c) + // }) - protectedRetribusiGroup.PUT("/id/:id", func(c *gin.Context) { - retribusiHandler.UpdateRetribusi(c) - }) + // protectedRetribusiGroup.PUT("/id/:id", func(c *gin.Context) { + // retribusiHandler.UpdateRetribusi(c) + // }) - protectedRetribusiGroup.DELETE("/id/:id", func(c *gin.Context) { - retribusiHandler.DeleteRetribusi(c) - }) - } + // protectedRetribusiGroup.DELETE("/id/:id", func(c *gin.Context) { + // retribusiHandler.DeleteRetribusi(c) + // }) + // } return router } diff --git a/tools/bpjs/generate-handler.go b/tools/bpjs/generate-handler.go new file mode 100644 index 0000000..1b4d8c4 --- /dev/null +++ b/tools/bpjs/generate-handler.go @@ -0,0 +1,3651 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + "time" + + "gopkg.in/yaml.v2" +) + +// Enhanced structures untuk validasi +type HandlerValidation struct { + ExistingFunctions map[string]bool + NewFunctions []string + UpdatedFiles []string + CreatedFiles []string +} + +type DirectoryInfo struct { + Path string + IsFile bool + Functions []FunctionInfo + Children map[string]*DirectoryInfo +} + +type FunctionInfo struct { + Name string + Methods []string + Endpoint string + Config EndpointConfig +} + +type EndpointConfig struct { + Methods []string `yaml:"methods"` + GetRoutes string `yaml:"get_routes"` + PostRoutes string `yaml:"post_routes"` + PutRoutes string `yaml:"put_routes"` + DeleteRoutes string `yaml:"delete_routes"` + GetPath string `yaml:"get_path"` + PostPath string `yaml:"post_path"` + PutPath string `yaml:"put_path"` + DeletePath string `yaml:"delete_path"` + Model string `yaml:"model"` + ResponseModel string `yaml:"response_model"` + RequestModel string `yaml:"request_model"` + 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"` + EnableDatabase bool `yaml:"enable_database"` +} + +type GlobalConfig struct { + ModuleName string `yaml:"module_name"` + OutputDir string `yaml:"output_dir"` + EnableSwagger bool `yaml:"enable_swagger"` + EnableLogging bool `yaml:"enable_logging"` +} + +type ServiceConfig struct { + Global GlobalConfig `yaml:"global,omitempty"` + Services map[string]Service `yaml:"services"` +} + +type Service 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]EndpointGroup `yaml:"endpoints"` + Dependencies string `yaml:"dependencies,omitempty"` + Middleware string `yaml:"middleware,omitempty"` +} + +type EndpointGroup struct { + Description string `yaml:"description"` + HandlerFolder string `yaml:"handler_folder"` + HandlerFile string `yaml:"handler_file"` + HandlerName string `yaml:"handler_name"` + Functions map[string]FunctionConfig `yaml:"functions"` +} + +type FunctionConfig struct { + Methods []string `yaml:"methods"` + Path string `yaml:"path"` + Model string `yaml:"model"` + RoutesLink string `yaml:"routes_link"` + // Routes untuk endpoint generation + GetRoutes string `yaml:"get_routes"` + PostRoutes string `yaml:"post_routes"` + PutRoutes string `yaml:"put_routes"` + DeleteRoutes string `yaml:"delete_routes"` + // ✅ Path untuk swagger documentation + GetPath string `yaml:"get_path"` + PostPath string `yaml:"post_path"` + PutPath string `yaml:"put_path"` + DeletePath string `yaml:"delete_path"` + ResponseModel string `yaml:"response_model"` + RequestModel string `yaml:"request_model"` + 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"` + EnableDatabase bool `yaml:"enable_database"` +} + +type TemplateData struct { + ServiceName string + ServiceLower string + ServiceUpper string + Category string + Package string + Description string + BaseURL string + Timeout int + RetryCount int + Endpoints []EndpointData + Timestamp string + ModuleName string + HasValidator bool + HasLogger bool + HasSwagger bool + GlobalConfig GlobalConfig + ShouldGenerateStruct bool + ShouldGenerateConstructor bool + FunctionalArea string + HandlerName string +} + +type EndpointData struct { + Name string + NameLower string + NameUpper string + NameCamel string + Methods []string + GetRoutes string + PostRoutes string + PutRoutes string + DeleteRoutes string + GetPath string + PostPath string + PutPath string + DeletePath string + Model string + ResponseModel string + RequestModel string + DataModel string + Description string + Summary string + Tags []string + HasGet bool + HasPost bool + HasPut bool + HasDelete bool + RequireAuth bool + CacheEnabled bool + EnableDatabase bool + CacheTTL int + PathParams []string + ModelPackage string +} + +// Fungsi utama yang diupdate untuk menggunakan separation of concerns +func generateHandlerWithValidation(serviceName string, svc Service, gc GlobalConfig) error { + // Step 1: Generate base handler file (struct + constructor) - SEKALI SAJA + // err := generateBaseHandlerFile(serviceName, svc, gc) + // if err != nil { + // return fmt.Errorf("generate base handler: %w", err) + // } + + // Step 2: Generate methods files per endpoint group + // baseDir := gc.OutputDir + // for groupName, grp := range svc.Endpoints { + // folder := filepath.Join(baseDir, grp.HandlerFolder) + // if err := os.MkdirAll(folder, 0755); err != nil { + // return fmt.Errorf("mkdir %s: %w", folder, err) + // } + + // // Generate methods file dengan naming yang jelas + // methodsFileName := fmt.Sprintf("%s.go", strings.ToLower(groupName)) + // methodsFilePath := filepath.Join(folder, methodsFileName) + + // // Check if methods file exists + // fileExists := false + // if _, err := os.Stat(methodsFilePath); err == nil { + // fileExists = true + // } + + // if !fileExists { + // // Create new methods file + // err := createMethodsFileFromConfig(methodsFilePath, svc, grp, gc, groupName) + // if err != nil { + // return fmt.Errorf("create methods file %s: %w", methodsFilePath, err) + // } + // fmt.Printf("✅ Created methods file: %s\n", methodsFilePath) + // } else { + // // Update existing methods file with new functions only + // err := updateExistingMethodsFile(methodsFilePath, svc, grp, gc, groupName) + // if err != nil { + // return fmt.Errorf("update methods file %s: %w", methodsFilePath, err) + // } + // fmt.Printf("✅ Updated methods file: %s\n", methodsFilePath) + // } + // } + // ✅ Step 2: Generate routes + err := generateRoutes(serviceName, svc, gc) + if err != nil { + return fmt.Errorf("generate routes: %w", err) + } + + return nil +} + +// Create new methods file +func createMethodsFileFromConfig(filePath string, svc Service, grp EndpointGroup, gc GlobalConfig, functionalArea string) error { + // Collect all functions for this group + var allEndpoints []EndpointData + for fname, fcfg := range grp.Functions { + td := processFunctionData(svc, grp, fname, fcfg, gc) + allEndpoints = append(allEndpoints, td.Endpoints...) + } + + templateData := TemplateData{ + ServiceName: svc.Name, + ServiceLower: strings.ToLower(svc.Name), + ServiceUpper: strings.ToUpper(svc.Name), + Category: svc.Category, + Package: grp.HandlerFolder, + Description: svc.Description, + BaseURL: svc.BaseURL, + Timeout: svc.Timeout, + RetryCount: svc.RetryCount, + Endpoints: allEndpoints, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + ModuleName: gc.ModuleName, + HasValidator: true, + HasLogger: gc.EnableLogging, + HasSwagger: gc.EnableSwagger, + GlobalConfig: gc, + ShouldGenerateStruct: false, // NEVER generate struct in methods file + ShouldGenerateConstructor: false, // NEVER generate constructor in methods file + FunctionalArea: functionalArea, + HandlerName: grp.HandlerName, // PERBAIKAN: Pastikan HandlerName diset + } + + return createMethodsFile(filePath, templateData) +} + +// Create methods file using methods-only template +func createMethodsFile(filePath string, templateData TemplateData) error { + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return err + } + + tmpl := template.New("methods").Funcs(template.FuncMap{ + "title": strings.Title, + "index": func(slice []string, i int) string { + if i >= 0 && i < len(slice) { + return slice[i] + } + return "" + }, + }) + + tmpl, err := tmpl.Parse(handlerTemplate) + if err != nil { + return err + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + return tmpl.Execute(file, templateData) +} + +// Update existing methods file (sama seperti sebelumnya tapi tanpa struct) +func updateExistingMethodsFile(filePath string, svc Service, grp EndpointGroup, gc GlobalConfig, functionalArea string) error { + existingContent, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + content := string(existingContent) + // Check for existing functions and collect new ones + var newEndpoints []EndpointData + + for fname, fcfg := range grp.Functions { + functionExists := false + for _, method := range fcfg.Methods { + funcName := generateFunctionName(fname, method) + // PERBAIKAN: Gunakan grp.HandlerName bukan svc.Name + sig := fmt.Sprintf("func (h *%sHandler) %s", grp.HandlerName, funcName) + if strings.Contains(content, sig) { + fmt.Printf("⚠️ Skip existing: %s (%s)\n", fname, funcName) + functionExists = true + break + } + } + + if functionExists { + continue + } + + td := processFunctionData(svc, grp, fname, fcfg, gc) + newEndpoints = append(newEndpoints, td.Endpoints...) + fmt.Printf("✅ Will add: %s\n", fname) + } + + if len(newEndpoints) == 0 { + fmt.Printf("⏭️ No new functions to add\n") + return nil + } + + // Generate new functions using methods-only template + templateData := TemplateData{ + ServiceName: svc.Name, + ServiceLower: strings.ToLower(svc.Name), + ServiceUpper: strings.ToUpper(svc.Name), + Category: svc.Category, + Package: grp.HandlerFolder, + Description: svc.Description, + BaseURL: svc.BaseURL, + Timeout: svc.Timeout, + RetryCount: svc.RetryCount, + Endpoints: newEndpoints, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + ModuleName: gc.ModuleName, + HasValidator: true, + HasLogger: gc.EnableLogging, + HasSwagger: gc.EnableSwagger, + GlobalConfig: gc, + ShouldGenerateStruct: false, // NEVER + ShouldGenerateConstructor: false, // NEVER + FunctionalArea: functionalArea, + HandlerName: grp.HandlerName, // PERBAIKAN: Pastikan HandlerName diset + } + + newFunctions, err := generateNewMethodsOnly(templateData) + if err != nil { + return err + } + + // Merge content + mergedContent := mergeGoFileContent(content, newFunctions) + return ioutil.WriteFile(filePath, []byte(mergedContent), 0644) +} + +// Generate new methods using methods-only template +func generateNewMethodsOnly(templateData TemplateData) (string, error) { + tmpl := template.New("newMethods") + tmpl, err := tmpl.Parse(handlerTemplate) + if err != nil { + return "", err + } + + var result strings.Builder + err = tmpl.Execute(&result, templateData) + if err != nil { + return "", err + } + + return result.String(), nil +} + +// Helper toCamelCase +func toCamelCase(s string) string { + parts := strings.FieldsFunc(s, func(r rune) bool { + return r == '_' || r == '-' || r == ' ' + }) + for i, p := range parts { + parts[i] = strings.Title(strings.ToLower(p)) + } + return strings.Join(parts, "") +} + +// Parse struktur direktori dari endpoints YAML +func parseDirectoryStructure(endpoints map[string]interface{}, serviceName string) *DirectoryInfo { + root := &DirectoryInfo{ + Path: "", + Children: make(map[string]*DirectoryInfo), + } + + for key, value := range endpoints { + parseNestedEndpoints(root, key, value) + } + + return root +} + +func parseNestedEndpoints(parent *DirectoryInfo, name string, value interface{}) { + switch v := value.(type) { + case map[string]interface{}: + // Check if this contains direct endpoint config + if isDirectEndpoint(v) { + // This is a direct endpoint - create as file + parent.Children[name] = &DirectoryInfo{ + Path: name, + IsFile: true, + Functions: []FunctionInfo{parseEndpointToFunction(name, v)}, + Children: make(map[string]*DirectoryInfo), + } + } else { + // This is nested structure - create as directory or file + child := &DirectoryInfo{ + Path: name, + IsFile: false, + Functions: make([]FunctionInfo, 0), + Children: make(map[string]*DirectoryInfo), + } + + // Check if any direct children are endpoints + hasDirectEndpoints := false + for childName, childValue := range v { + if childMap, ok := childValue.(map[string]interface{}); ok && isDirectEndpoint(childMap) { + hasDirectEndpoints = true + child.Functions = append(child.Functions, parseEndpointToFunction(childName, childMap)) + } + } + + if hasDirectEndpoints { + child.IsFile = true + } + + parent.Children[name] = child + + // Recursively parse nested children + for childName, childValue := range v { + if childMap, ok := childValue.(map[string]interface{}); ok && !isDirectEndpoint(childMap) { + parseNestedEndpoints(child, childName, childValue) + } + } + } + } +} + +func isDirectEndpoint(m map[string]interface{}) bool { + _, hasMethods := m["methods"] + _, hasGetPath := m["get_path"] + _, hasPostPath := m["post_path"] + return hasMethods || hasGetPath || hasPostPath +} + +func parseEndpointToFunction(name string, config map[string]interface{}) FunctionInfo { + function := FunctionInfo{ + Name: name, + Endpoint: name, + Methods: make([]string, 0), + } + + if methods, ok := config["methods"].([]interface{}); ok { + for _, method := range methods { + if methodStr, ok := method.(string); ok { + function.Methods = append(function.Methods, strings.ToUpper(strings.TrimSpace(methodStr))) + } + } + } else if methodStr, ok := config["methods"].(string); ok { + // Handle case where methods is a string like "GET,POST" + methods := strings.Split(methodStr, ",") + for _, method := range methods { + function.Methods = append(function.Methods, strings.ToUpper(strings.TrimSpace(method))) + } + } + + return function +} + +// Process directory structure dan generate files +func processDirectoryStructure(baseDir string, dirInfo *DirectoryInfo, service Service, globalConfig GlobalConfig, validation *HandlerValidation) error { + for name, child := range dirInfo.Children { + currentPath := filepath.Join(baseDir, child.Path) + + if child.IsFile { + // Process as file + err := processHandlerFile(currentPath, name, child, service, globalConfig, validation) + if err != nil { + return fmt.Errorf("failed to process file %s: %w", name, err) + } + } else { + // Create directory dan process children + if err := os.MkdirAll(currentPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", currentPath, err) + } + + err := processDirectoryStructure(currentPath, child, service, globalConfig, validation) + if err != nil { + return err + } + } + } + + return nil +} +func processFunctionData(svc Service, grp EndpointGroup, fname string, fcfg FunctionConfig, gc GlobalConfig) TemplateData { + ed := EndpointData{ + Name: toCamelCase(fname), + NameLower: strings.ToLower(fname), + NameUpper: strings.ToUpper(fname), + NameCamel: toCamelCase(fname), + Methods: fcfg.Methods, + GetRoutes: fcfg.GetRoutes, + PostRoutes: fcfg.PostRoutes, + PutRoutes: fcfg.PutRoutes, + DeleteRoutes: fcfg.DeleteRoutes, + GetPath: fcfg.Path, + PostPath: fcfg.Path, + PutPath: fcfg.Path, + DeletePath: fcfg.Path, + Model: fcfg.Model, + ResponseModel: fcfg.ResponseModel, + RequestModel: fcfg.RequestModel, + DataModel: strings.Replace(fcfg.ResponseModel, "Response", "Data", 1), + Description: fcfg.Description, + Summary: fcfg.Summary, + Tags: fcfg.Tags, + RequireAuth: fcfg.RequireAuth, + CacheEnabled: fcfg.CacheEnabled, + CacheTTL: fcfg.CacheTTL, + EnableDatabase: fcfg.EnableDatabase, + PathParams: extractPathParams(fcfg.Path), + ModelPackage: grp.HandlerFolder, + } + // set flags + for _, m := range fcfg.Methods { + switch strings.ToUpper(m) { + case "GET": + ed.HasGet = true + case "POST": + ed.HasPost = true + case "PUT": + ed.HasPut = true + case "DELETE": + ed.HasDelete = true + } + } + return TemplateData{ + ServiceName: svc.Name, + ServiceLower: strings.ToLower(svc.Name), + ServiceUpper: strings.ToUpper(svc.Name), + Category: svc.Category, + Package: grp.HandlerFolder, + Description: svc.Description, + BaseURL: svc.BaseURL, + Timeout: svc.Timeout, + RetryCount: svc.RetryCount, + Endpoints: []EndpointData{ed}, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + ModuleName: gc.ModuleName, + HasValidator: true, + HasLogger: gc.EnableLogging, + HasSwagger: gc.EnableSwagger, + GlobalConfig: gc, + HandlerName: grp.HandlerName, + } +} + +// extractPathParams (jika belum ada) +func extractPathParams(path string) []string { + var ps []string + for _, part := range strings.Split(path, "/") { + if strings.HasPrefix(part, ":") { + ps = append(ps, strings.TrimPrefix(part, ":")) + } + } + return ps +} + +// Process individual handler file - PERBAIKAN: Hapus referensi grp.HandlerName +func processHandlerFile(basePath, fileName string, dirInfo *DirectoryInfo, service Service, globalConfig GlobalConfig, validation *HandlerValidation) error { + filePath := filepath.Join(filepath.Dir(basePath), fmt.Sprintf("%s.go", fileName)) + fmt.Printf("📄 Processing file: %s\n", filePath) + + // Check if file exists + fileExists := false + if _, err := os.Stat(filePath); err == nil { + fileExists = true + fmt.Printf(" 📋 File exists, checking functions...\n") + } + + var existingFunctions map[string]bool + if fileExists { + // Parse existing functions - PERBAIKAN: Hanya gunakan 1 parameter + functions, err := extractExistingFunctions(filePath) + if err != nil { + fmt.Printf(" ⚠️ Warning: Could not parse existing file: %v\n", err) + existingFunctions = make(map[string]bool) + } else { + existingFunctions = functions + fmt.Printf(" ✓ Found %d existing functions\n", len(functions)) + } + } else { + existingFunctions = make(map[string]bool) + } + + // Determine which functions to generate + functionsToGenerate := make([]FunctionInfo, 0) + for _, fnInfo := range dirInfo.Functions { + for _, method := range fnInfo.Methods { + functionName := generateFunctionName(fnInfo.Name, method) + if !existingFunctions[functionName] { + functionsToGenerate = append(functionsToGenerate, FunctionInfo{ + Name: fnInfo.Name, + Methods: []string{method}, + Endpoint: fnInfo.Endpoint, + }) + validation.NewFunctions = append(validation.NewFunctions, functionName) + fmt.Printf(" ✅ Will generate: %s\n", functionName) + } else { + fmt.Printf(" ⏭️ Already exists: %s\n", functionName) + } + } + } + + // Generate file if needed + if len(functionsToGenerate) > 0 { + templateData := prepareTemplateData(fileName, service, globalConfig, functionsToGenerate) + + if fileExists { + // Merge with existing file + err := mergeWithExistingFile(filePath, templateData) + if err != nil { + return fmt.Errorf("failed to merge file %s: %w", filePath, err) + } + validation.UpdatedFiles = append(validation.UpdatedFiles, filePath) + fmt.Printf(" 📝 Updated existing file\n") + } else { + // Create new file + err := createNewHandlerFile(filePath, templateData) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", filePath, err) + } + validation.CreatedFiles = append(validation.CreatedFiles, filePath) + fmt.Printf(" 📁 Created new file\n") + } + } else if !fileExists { + fmt.Printf(" ⏭️ No functions to generate and file doesn't exist\n") + } else { + fmt.Printf(" ⏭️ All functions already exist\n") + } + + return nil +} + +// Extract existing functions from Go file +func extractExistingFunctions(filePath string) (map[string]bool, error) { + fileSet := token.NewFileSet() + node, err := parser.ParseFile(fileSet, filePath, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + functions := make(map[string]bool) + ast.Inspect(node, func(n ast.Node) bool { + if fn, ok := n.(*ast.FuncDecl); ok { + if fn.Name != nil { + functions[fn.Name.Name] = true + } + } + return true + }) + return functions, nil +} + +// Generate function name berdasarkan endpoint dan method +func generateFunctionName(endpointName, method string) string { + switch strings.ToUpper(method) { + case "GET": + return fmt.Sprintf("Get%s", strings.Title(endpointName)) + case "POST": + return fmt.Sprintf("Create%s", strings.Title(endpointName)) + case "PUT": + return fmt.Sprintf("Update%s", strings.Title(endpointName)) + case "DELETE": + return fmt.Sprintf("Delete%s", strings.Title(endpointName)) + case "PATCH": + return fmt.Sprintf("Patch%s", strings.Title(endpointName)) + default: + return fmt.Sprintf("%s%s", strings.Title(method), strings.Title(endpointName)) + } +} + +// Prepare template data +func prepareTemplateData(packageName string, service Service, globalConfig GlobalConfig, functions []FunctionInfo) TemplateData { + endpoints := make([]EndpointData, 0) + + for _, fnInfo := range functions { + endpoint := EndpointData{ + Name: strings.Title(fnInfo.Name), + NameLower: strings.ToLower(fnInfo.Name), + NameUpper: strings.ToUpper(fnInfo.Name), + NameCamel: toCamelCase(fnInfo.Name), + ModelPackage: packageName, + Model: fmt.Sprintf("%sRequest", strings.Title(fnInfo.Name)), + ResponseModel: fmt.Sprintf("%sResponse", strings.Title(fnInfo.Name)), + RequestModel: fmt.Sprintf("%sRequest", strings.Title(fnInfo.Name)), + DataModel: fmt.Sprintf("%sData", strings.Title(fnInfo.Name)), + Description: fmt.Sprintf("Handle %s operations", fnInfo.Name), + Tags: []string{strings.Title(packageName)}, + } + + // Set paths dan methods + for _, method := range fnInfo.Methods { + switch strings.ToUpper(method) { + case "GET": + endpoint.HasGet = true + endpoint.GetPath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) + case "POST": + endpoint.HasPost = true + endpoint.PostPath = fmt.Sprintf("/%s", packageName) + case "PUT": + endpoint.HasPut = true + endpoint.PutPath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) + case "DELETE": + endpoint.HasDelete = true + endpoint.DeletePath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) + } + } + + endpoints = append(endpoints, endpoint) + } + + return TemplateData{ + ServiceName: service.Name, + ServiceLower: strings.ToLower(service.Name), + ServiceUpper: strings.ToUpper(service.Name), + Category: service.Category, + Package: packageName, + Description: service.Description, + BaseURL: service.BaseURL, + Timeout: getOrDefault(service.Timeout, 30), + RetryCount: getOrDefault(service.RetryCount, 3), + Endpoints: endpoints, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + ModuleName: globalConfig.ModuleName, + HasValidator: true, + HasLogger: globalConfig.EnableLogging, + HasSwagger: globalConfig.EnableSwagger, + GlobalConfig: globalConfig, + HandlerName: strings.Title(packageName), + } +} + +// Check if handler struct already exists in directory +func shouldGenerateHandlerStruct(baseDir, handlerName string) bool { + structSignature := fmt.Sprintf("type %sHandler struct", handlerName) + + files, err := ioutil.ReadDir(baseDir) + if err != nil { + return true // If directory doesn't exist, generate struct + } + + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") { + continue + } + + filePath := filepath.Join(baseDir, file.Name()) + content, err := ioutil.ReadFile(filePath) + if err != nil { + continue + } + + if strings.Contains(string(content), structSignature) { + return false // Struct already exists + } + } + + return true // Struct not found, should generate +} + +// Check if constructor already exists +func shouldGenerateConstructor(baseDir, handlerName string) bool { + constructorSignature := fmt.Sprintf("func New%sHandler", handlerName) + + files, err := ioutil.ReadDir(baseDir) + if err != nil { + return true + } + + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") { + continue + } + + filePath := filepath.Join(baseDir, file.Name()) + content, err := ioutil.ReadFile(filePath) + if err != nil { + continue + } + + if strings.Contains(string(content), constructorSignature) { + return false + } + } + + return true +} + +// Generate base handler file (struct + constructor only) +func generateBaseHandlerFile(serviceName string, svc Service, gc GlobalConfig) error { + baseDir := gc.OutputDir + if err := os.MkdirAll(baseDir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", baseDir, err) + } + + baseFileName := fmt.Sprintf("%s_base.go", strings.ToLower(serviceName)) + baseFilePath := filepath.Join(baseDir, baseFileName) + + // Skip if base file already exists + if _, err := os.Stat(baseFilePath); err == nil { + fmt.Printf("⏭️ Base handler already exists: %s\n", baseFilePath) + return nil + } + + templateData := TemplateData{ + ServiceName: svc.Name, + ServiceLower: strings.ToLower(svc.Name), + ServiceUpper: strings.ToUpper(svc.Name), + Category: svc.Category, + Package: "handlers", + Description: svc.Description, + BaseURL: svc.BaseURL, + Timeout: svc.Timeout, + RetryCount: svc.RetryCount, + Endpoints: []EndpointData{}, // Empty - base only + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + ModuleName: gc.ModuleName, + HasValidator: true, + HasLogger: gc.EnableLogging, + HasSwagger: gc.EnableSwagger, + GlobalConfig: gc, + } + + return createBaseHandlerFile(baseFilePath, templateData) +} + +// Template untuk base handler (struct + constructor only) +const baseHandlerTemplate = `// Package handlers handles {{.HandlerName}} BPJS services - Base Handler +// Generated on: {{.Timestamp}} +package handlers + +import ( + "{{.ModuleName}}/internal/config" + "{{.ModuleName}}/internal/services/bpjs" + "{{.ModuleName}}/pkg/logger" + + "github.com/go-playground/validator/v10" +) + +// {{.HandlerName}}Handler handles {{.HandlerName}} BPJS services +type {{.HandlerName}}Handler struct { + service services.VClaimService + db database.Service + validator *validator.Validate + logger logger.Logger + config config.BpjsConfig +} + +// {{.HandlerName}}HandlerConfig contains configuration for {{.HandlerName}}Handler +type {{.HandlerName}}HandlerConfig struct { + Config *config.Config + Logger logger.Logger + Validator *validator.Validate +} + +// New{{.HandlerName}}Handler creates a new {{.HandlerName}}Handler +func New{{.HandlerName}}Handler(cfg {{.HandlerName}}HandlerConfig) *{{.HandlerName}}Handler { + return &{{.HandlerName}}Handler{ + db: database.New(cfg.Config), + service: services.NewService(cfg.Config.Bpjs), + validator: cfg.Validator, + logger: cfg.Logger, + config: cfg.Config.Bpjs, + } +} +` + +// Function to create base handler file +func createBaseHandlerFile(filePath string, templateData TemplateData) error { + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return err + } + + tmpl := template.New("baseHandler").Funcs(template.FuncMap{ + "title": strings.Title, + }) + + tmpl, err := tmpl.Parse(baseHandlerTemplate) + if err != nil { + return err + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + return tmpl.Execute(file, templateData) +} + +// Template untuk handler file lengkap (untuk file baru) +const handlerTemplate = `// Package {{.Package}} handles {{.HandlerName}} BPJS services +// Generated on: {{.Timestamp}} +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "time" + + "{{.ModuleName}}/internal/config" + "{{.ModuleName}}/internal/models" + "{{.ModuleName}}/internal/database" + "{{.ModuleName}}/internal/models/{{.Package}}" + "{{.ModuleName}}/internal/services/bpjs" + "{{.ModuleName}}/pkg/logger" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" +) +// {{.HandlerName}}Handler handles {{.HandlerName}} BPJS services +type {{.HandlerName}}Handler struct { + service services.VClaimService + db database.Service + validator *validator.Validate + logger logger.Logger + config config.BpjsConfig +} +// {{.HandlerName}}HandlerConfig contains configuration for {{.HandlerName}}Handler +type {{.HandlerName}}HandlerConfig struct { + Config *config.Config + Logger logger.Logger + Validator *validator.Validate +} +// New{{.HandlerName}}Handler creates a new {{.HandlerName}}Handler +func New{{.HandlerName}}Handler(cfg {{.HandlerName}}HandlerConfig) *{{.HandlerName}}Handler { + return &{{.HandlerName}}Handler{ + db: database.New(cfg.Config), + service: services.NewService(cfg.Config.Bpjs), + validator: cfg.Validator, + logger: cfg.Logger, + config: cfg.Config.Bpjs, + } +} +{{range .Endpoints}} +{{if .HasGet}} +// Get{{.Name}} godoc +// @Summary Get {{.Name}} data +// @Description {{.Description}} +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.GetRoutes}} [get] +func (h *{{$.HandlerName}}Handler) Get{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.GetPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.GetRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + // Ambil status code dari metaData.code + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasPost}} +// Create{{.Name}} godoc +// @Summary Create new {{.Name}} +// @Description Create new {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" +// @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PostRoutes}} [post] +func (h *{{$.HandlerName}}Handler) Create{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Create{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PostPath}}", + }) + {{end}} + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + {{if .PathParams}} + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + endpoint := "{{.PostPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + resp, err := h.service.PostRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} already exists or conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + + +{{if .HasPut}} +// Update{{.Name}} godoc +// @Summary Update existing {{.Name}} +// @Description Update existing {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PutRoutes}} [put] +func (h *{{$.HandlerName}}Handler) Update{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PutPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} +{{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.PutPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.PutRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "Update conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasDelete}} +// Delete{{.Name}} godoc +// @Summary Delete existing {{.Name}} +// @Description Delete existing {{.Name}} from BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.DeleteRoutes}} [delete] +func (h *{{$.HandlerName}}Handler) Delete{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.DeletePath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.DeletePath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.DeleteRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } else { + // For delete operations, sometimes there's no data in response + response.Data = nil + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} +{{end}}` + +// Template untuk menambah function saja (untuk file yang sudah ada) +const functionsOnlyTemplate = `{{range .Endpoints}} +{{if .HasGet}} +// Get{{.Name}} godoc +// @Summary Get {{.Name}} data +// @Description {{.Description}} +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.GetRoutes}} [get] +func (h *{{$.HandlerName}}Handler) Get{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.GetPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.GetRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasPost}} +// Create{{.Name}} godoc +// @Summary Create new {{.Name}} +// @Description Create new {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" +// @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PostRoutes}} [post] +func (h *{{$.HandlerName}}Handler) Create{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Create{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PostPath}}", + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + {{if .PathParams}} + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + endpoint := "{{.PostPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + resp, err := h.service.PostRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} already exists or conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + + +{{if .HasPut}} +// Update{{.Name}} godoc +// @Summary Update existing {{.Name}} +// @Description Update existing {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PutRoutes}} [put] +func (h *{{$.HandlerName}}Handler) Update{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PutPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.PutPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.PutRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "Update conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasDelete}} +// Delete{{.Name}} godoc +// @Summary Delete existing {{.Name}} +// @Description Delete existing {{.Name}} from BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.DeleteRoutes}} [delete] +func (h *{{$.HandlerName}}Handler) Delete{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.DeletePath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.DeletePath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.DeleteRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } else { + // For delete operations, sometimes there's no data in response + response.Data = nil + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} +{{end}}` + +// Create new handler file +func createNewHandlerFile(filePath string, templateData TemplateData) error { + // Create directory + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return err + } + + // Parse template + tmpl := template.New("handler").Funcs(template.FuncMap{ + "title": strings.Title, + "index": func(slice []string, i int) string { + if i >= 0 && i < len(slice) { + return slice[i] + } + return "" + }, + }) + + tmpl, err := tmpl.Parse(handlerTemplate) + if err != nil { + return err + } + + // Create file + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + // Execute template + return tmpl.Execute(file, templateData) +} + +// Merge dengan existing file +func mergeWithExistingFile(filePath string, templateData TemplateData) error { + // Read existing content + existingContent, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + // Generate new functions + newFunctions, err := generateNewFunctionsOnly(templateData) + if err != nil { + return err + } + + // Merge content + mergedContent := mergeGoFileContent(string(existingContent), newFunctions) + + // Write back + return ioutil.WriteFile(filePath, []byte(mergedContent), 0644) +} + +func generateNewFunctionsOnly(templateData TemplateData) (string, error) { + funcTemplate := ` +{{range .Endpoints}} +{{if .HasGet}} +// Get{{.Name}} godoc +// @Summary Get {{.Name}} data +// @Description {{.Description}} +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.GetRoutes}} [get] +func (h *{{$.HandlerName}}Handler) Get{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.GetPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.GetRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasPost}} +// Create{{.Name}} godoc +// @Summary Create new {{.Name}} +// @Description Create new {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" +// @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PostRoutes}} [post] +func (h *{{$.HandlerName}}Handler) Create{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Create{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PostPath}}", + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + {{if .PathParams}} + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + endpoint := "{{.PostPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + resp, err := h.service.PostRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} already exists or conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + + +{{if .HasPut}} +// Update{{.Name}} godoc +// @Summary Update existing {{.Name}} +// @Description Update existing {{.Name}} in BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.PutRoutes}} [put] +func (h *{{$.HandlerName}}Handler) Update{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.PutPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Bind and validate request body + var req {{.ModelPackage}}.{{.RequestModel}} + if err := c.ShouldBindJSON(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to bind request body", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Invalid request body: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Validate request structure + if err := h.validator.Struct(&req); err != nil { + {{if $.HasLogger}} + h.logger.Error("Request validation failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Validation failed: " + err.Error(), + RequestID: requestID, + }) + return + } + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.PutPath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.PutRawResponse(ctx, endpoint, req) + {{else}} + resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ + Status: "error", + Message: "Update conflict occurred", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} + +{{if .HasDelete}} +// Delete{{.Name}} godoc +// @Summary Delete existing {{.Name}} +// @Description Delete existing {{.Name}} from BPJS system +// @Tags {{index .Tags 0}} +// @Accept json +// @Produce json {{if .RequireAuth}} +// @Security ApiKeyAuth {{end}} +// @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} +// @Param {{.}} path string true "{{.}}" example("example_value") {{end}} +// @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" +// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" +// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" +// @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" +// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" +// @Router {{.DeleteRoutes}} [delete] +func (h *{{$.HandlerName}}Handler) Delete{{.Name}}(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) + defer cancel() + + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + {{if $.HasLogger}} + h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.DeletePath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if $.HasLogger}} + h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ + "request_id": requestID, + "endpoint": "{{.GetPath}}", + {{range .PathParams}} + "{{.}}": c.Param("{{.}}"), + {{end}} + }) + {{end}} + + {{if .EnableDatabase}} + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Database connection failed", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Database connection failed", + RequestID: requestID, + }) + return + } + // Note: dbConn is available for future database operations (e.g., caching, logging) + _ = dbConn // Prevent unused variable warning + {{end}} + + // Extract path parameters + {{range .PathParams}} + {{.}} := c.Param("{{.}}") + if {{.}} == "" { + {{if $.HasLogger}} + h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ + Status: "error", + Message: "Missing required parameter {{.}}", + RequestID: requestID, + }) + return + } + {{end}} + + // Call service method + var response {{.ModelPackage}}.{{.ResponseModel}} + {{if .PathParams}} + endpoint := "{{.DeletePath}}" + {{range .PathParams}} + endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) + {{end}} + resp, err := h.service.DeleteRawResponse(ctx, endpoint) + {{else}} + resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") + {{end}} + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + + // Handle specific BPJS errors + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { + c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ + Status: "error", + Message: "{{.Name}} not found", + RequestID: requestID, + }) + return + } + + c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ + Status: "error", + Message: "Internal server error", + RequestID: requestID, + }) + return + } + + // Map the raw response + response.MetaData = resp.MetaData + if resp.Response != nil { + response.Data = &{{.ModelPackage}}.{{.DataModel}}{} + if respStr, ok := resp.Response.(string); ok { + // Decrypt the response string + consID, secretKey, _, tstamp, _ := h.config.SetHeader() + decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) + if err != nil { + {{if $.HasLogger}} + h.logger.Error("Failed to decrypt response", map[string]interface{}{ + "error": err.Error(), + "request_id": requestID, + }) + {{end}} + } else { + json.Unmarshal([]byte(decryptedResp), response.Data) + } + } else if respMap, ok := resp.Response.(map[string]interface{}); ok { + // Response is already unmarshaled JSON + if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { + dataBytes, _ := json.Marshal(dataMap) + json.Unmarshal(dataBytes, response.Data) + } else { + // Try to unmarshal the whole response + respBytes, _ := json.Marshal(resp.Response) + json.Unmarshal(respBytes, response.Data) + } + } + } else { + // For delete operations, sometimes there's no data in response + response.Data = nil + } + + // Ensure response has proper fields + response.Status = "success" + response.RequestID = requestID + + {{if $.HasLogger}} + h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ + "request_id": requestID, + }) + {{end}} + + var statusCode int + code := models.ExtractCode(response.MetaData) + if code != nil { + statusCode = models.GetStatusCodeFromMeta(code) + } else { + statusCode = 200 + } + c.JSON(statusCode, response) +} +{{end}} +{{end}}` + + tmpl := template.New("functions") + tmpl, err := tmpl.Parse(funcTemplate) + if err != nil { + return "", err + } + + var result strings.Builder + err = tmpl.Execute(&result, templateData) + if err != nil { + return "", err + } + + // Remove duplicate function definitions by simple regex grouping + // This is a simple approach to avoid duplicate functions in merged content + funcRegex := regexp.MustCompile(`(?s)(func \(h \*\w+Handler\) Get\w+\(c \*gin.Context\) \{.*?\})`) + matches := funcRegex.FindAllString(result.String(), -1) + uniqueFuncs := make(map[string]bool) + var uniqueResult strings.Builder + for _, m := range matches { + if !uniqueFuncs[m] { + uniqueFuncs[m] = true + uniqueResult.WriteString(m) + uniqueResult.WriteString("\n\n") + } + } + + // If no matches found, return original result + if uniqueResult.Len() == 0 { + return result.String(), nil + } + + return uniqueResult.String(), nil +} + +func mergeGoFileContent(existingContent, newFunctions string) string { + // Find last closing brace + re := regexp.MustCompile(`}\s*$`) + lastBraceIndex := re.FindStringIndex(existingContent) + + if lastBraceIndex == nil { + return existingContent + "\n" + newFunctions + } + + before := existingContent[:lastBraceIndex[0]] + after := existingContent[lastBraceIndex[0]:] + + return before + "\n" + newFunctions + "\n" + after +} + +// Main function +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + configFile := os.Args[1] + var targetService string + if len(os.Args) > 2 { + targetService = os.Args[2] + } + + config, err := loadConfig(configFile) + if err != nil { + fmt.Printf("Error loading config: %v\n", err) + os.Exit(1) + } + + fmt.Println("🚀 Starting BPJS Dynamic Handler Generation with Validation...") + fmt.Printf("📁 Config file: %s\n", configFile) + if targetService != "" { + fmt.Printf("🎯 Target service: %s\n", targetService) + } + + generated := 0 + errors := 0 + + for serviceName, service := range config.Services { + if targetService != "" && serviceName != targetService { + continue + } + + err := generateHandlerWithValidation(serviceName, service, config.Global) + if err != nil { + fmt.Printf("❌ Error generating handler for %s: %v\n", serviceName, err) + errors++ + continue + } + + generated++ + } + + // Summary + fmt.Println("\n📊 Generation Summary:") + fmt.Printf("✅ Successfully processed: %d services\n", generated) + if errors > 0 { + fmt.Printf("❌ Failed: %d services\n", errors) + } + + if generated > 0 { + fmt.Println("🎉 Generation completed successfully!") + } +} + +// Helper functions +func loadConfig(filename string) (*ServiceConfig, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var config ServiceConfig + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + // Set defaults + if config.Global.ModuleName == "" { + config.Global.ModuleName = "api-service" + } + if config.Global.OutputDir == "" { + config.Global.OutputDir = "internal/handlers" + } + + return &config, nil +} + +func getOrDefault(value, defaultValue int) int { + if value == 0 { + return defaultValue + } + return value +} + +func printUsage() { + fmt.Println("BPJS Dynamic Handler Generator with Function Validation") + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" go run generate-handler.go [service-name]") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" go run generate-handler.go services-config-bpjs.yaml") + fmt.Println(" go run generate-handler.go services-config-bpjs.yaml vclaim") +} + +// Generate routes file untuk service +func generateRoutes(serviceName string, svc Service, gc GlobalConfig) error { + routesFilePath := "internal/routes/v1/routes.go" + routesContent, err := ioutil.ReadFile(routesFilePath) + if err != nil { + return fmt.Errorf("failed to read routes file: %w", err) + } + + routesContentStr := string(routesContent) + + var newImports []string + var allRoutes []string + + // Track processed folders to avoid duplicate imports + processedFolders := make(map[string]bool) + + for groupName, grp := range svc.Endpoints { + // Check and add import if not exists + importLine := fmt.Sprintf("%sHandlers \"%s/internal/handlers/%s\"", + grp.HandlerFolder, gc.ModuleName, grp.HandlerFolder) + if !processedFolders[grp.HandlerFolder] && !strings.Contains(routesContentStr, importLine) { + newImports = append(newImports, fmt.Sprintf("\t%s", importLine)) + processedFolders[grp.HandlerFolder] = true + fmt.Printf("✅ Will add import: %sHandlers\n", grp.HandlerFolder) + } else if processedFolders[grp.HandlerFolder] { + fmt.Printf("⚠️ Skipped duplicate import for folder: %s\n", grp.HandlerFolder) + } else { + fmt.Printf("⚠️ Import already exists: %sHandlers\n", grp.HandlerFolder) + } + + var routesCode strings.Builder + + // Comment for the group + routesCode.WriteString(fmt.Sprintf("\n\t// %s (%s) routes\n", grp.Description, groupName)) + + // Check if handler is already instantiated + handlerVar := strings.ToLower(grp.HandlerName) + "Handler" + handlerInstantiation := fmt.Sprintf("%s := %sHandlers.New%sHandler(%sHandlers.%sHandlerConfig{", + handlerVar, grp.HandlerFolder, grp.HandlerName, grp.HandlerFolder, grp.HandlerName) + if !strings.Contains(routesContentStr, handlerInstantiation) { + routesCode.WriteString(fmt.Sprintf("\t%s := %sHandlers.New%sHandler(%sHandlers.%sHandlerConfig{\n", + handlerVar, grp.HandlerFolder, grp.HandlerName, grp.HandlerFolder, grp.HandlerName)) + routesCode.WriteString("\t\tConfig: cfg,\n") + routesCode.WriteString("\t\tLogger: *logger.Default(),\n") + routesCode.WriteString("\t\tValidator: validator.New(),\n") + routesCode.WriteString("\t})\n") + fmt.Printf("✅ Will add handler instantiation: %s\n", handlerVar) + } else { + fmt.Printf("⚠️ Handler already instantiated: %s\n", handlerVar) + } + + // Check if group is already created + groupVar := strings.ToLower(grp.HandlerName) + "Group" + groupCreation := fmt.Sprintf("%s := v1.Group(\"/%s\")", groupVar, groupName) + if !strings.Contains(routesContentStr, groupCreation) { + routesCode.WriteString(fmt.Sprintf("\t%s := v1.Group(\"/%s\")\n", groupVar, groupName)) + fmt.Printf("✅ Will add group: %s\n", groupVar) + } else { + fmt.Printf("⚠️ Group already exists: %s\n", groupVar) + } + + // Process functions + for fname, fcfg := range grp.Functions { + td := processFunctionData(svc, grp, fname, fcfg, gc) + + for _, endpoint := range td.Endpoints { + // Loop through methods and use specific routes + for _, method := range fcfg.Methods { + var cleanPath string + + // Select path based on method + switch strings.ToUpper(method) { + case "GET": + cleanPath = fcfg.GetRoutes + if cleanPath == "" { + cleanPath = fcfg.GetPath + } + case "POST": + cleanPath = fcfg.PostRoutes + if cleanPath == "" { + cleanPath = fcfg.PostPath + } + case "PUT": + cleanPath = fcfg.PutRoutes + if cleanPath == "" { + cleanPath = fcfg.PutPath + } + case "DELETE": + cleanPath = fcfg.DeleteRoutes + if cleanPath == "" { + cleanPath = fcfg.DeletePath + } + default: + fmt.Printf("⚠️ Unsupported HTTP method: %s for function %s\n", method, fname) + continue + } + + // Final fallback to path if specific route empty + if cleanPath == "" { + cleanPath = fcfg.Path + } + + // Clean path - remove prefix groupName if present and ensure lowercase + if strings.HasPrefix(cleanPath, "/"+groupName) { + cleanPath = strings.TrimPrefix(cleanPath, "/"+groupName) + } + if cleanPath == "" { + cleanPath = "/" + } + // Ensure path is lowercase for consistency + cleanPath = strings.ToLower(cleanPath) + + // Generate route based on method + var routeLine string + switch strings.ToUpper(method) { + case "GET": + routeLine = fmt.Sprintf("\t%s.GET(\"%s\", %s.Get%s)", groupVar, cleanPath, handlerVar, endpoint.Name) + case "POST": + routeLine = fmt.Sprintf("\t%s.POST(\"%s\", %s.Create%s)", groupVar, cleanPath, handlerVar, endpoint.Name) + case "PUT": + routeLine = fmt.Sprintf("\t%s.PUT(\"%s\", %s.Update%s)", groupVar, cleanPath, handlerVar, endpoint.Name) + case "DELETE": + routeLine = fmt.Sprintf("\t%s.DELETE(\"%s\", %s.Delete%s)", groupVar, cleanPath, handlerVar, endpoint.Name) + } + + // Check if route already exists + if !strings.Contains(routesContentStr, routeLine) { + routesCode.WriteString(routeLine + "\n") + fmt.Printf("✅ Will add route: %s %s\n", method, cleanPath) + } else { + fmt.Printf("⚠️ Skipped duplicate route: %s %s\n", method, cleanPath) + } + } + } + } + + if routesCode.Len() > 0 { + allRoutes = append(allRoutes, routesCode.String()) + } + } + + // Insert new imports if any + if len(newImports) > 0 { + importSection := strings.Join(newImports, "\n") + "\n" + + // Find the database import marker + databaseImportMarker := fmt.Sprintf("\"%s/internal/database\"", gc.ModuleName) + if idx := strings.Index(routesContentStr, databaseImportMarker); idx != -1 { + // Find end of that line + endOfLine := strings.Index(routesContentStr[idx:], "\n") + idx + 1 + routesContentStr = routesContentStr[:endOfLine] + importSection + routesContentStr[endOfLine:] + fmt.Printf("✅ Inserted new imports after database import\n") + } else { + // Fallback to after import ( + if idx := strings.Index(routesContentStr, "import ("); idx != -1 { + importStart := idx + len("import (") + routesContentStr = routesContentStr[:importStart] + "\n" + importSection + routesContentStr[importStart:] + fmt.Printf("⚠️ Database import not found, inserted after import (\n") + } + } + } + + // Find insertion point for routes + var insertionPoint int + newFormatMarker := "// =============================================================================\n// PUBLISHED ROUTES\n// =============================================================================" + if idx := strings.Index(routesContentStr, newFormatMarker); idx != -1 { + insertionPoint = idx + len(newFormatMarker) + fmt.Printf("✅ Found new format PUBLISHED ROUTES marker\n") + } else if idx := strings.Index(routesContentStr, "// PUBLISHED ROUTES"); idx != -1 { + insertionPoint = idx + len("// PUBLISHED ROUTES") + fmt.Printf("⚠️ Found old format PUBLISHED ROUTES marker\n") + } else { + return fmt.Errorf("PUBLISHED ROUTES marker not found in routes.go") + } + + // Insert new routes + if len(allRoutes) > 0 { + newRoutesContent := routesContentStr[:insertionPoint] + "\n" + strings.Join(allRoutes, "\n") + routesContentStr[insertionPoint:] + err = ioutil.WriteFile(routesFilePath, []byte(newRoutesContent), 0644) + if err != nil { + return fmt.Errorf("failed to write updated routes file: %w", err) + } + fmt.Printf("✅ Updated main routes file with new routes for %s\n", svc.Name) + } else { + fmt.Printf("⏭️ No new routes to add for %s\n", svc.Name) + } + + return nil +} diff --git a/tools/bpjs/services-config.yaml b/tools/bpjs/services-config.yaml new file mode 100644 index 0000000..6c80820 --- /dev/null +++ b/tools/bpjs/services-config.yaml @@ -0,0 +1,166 @@ +global: + module_name: "api-service" + output_dir: "internal/handlers" + enable_swagger: true + enable_logging: true + +services: + vclaim: + name: "VClaim" + category: "vclaim" + package: "vclaim" + description: "BPJS VClaim service for eligibility and SEP management" + base_url: "https://apijkn.bpjs-kesehatan.go.id/vclaim-rest" + timeout: 30 + retry_count: 3 + + endpoints: + peserta: + description: "Participant eligibility information" + handler_folder: "peserta" + handler_file: "peserta.go" + handler_name: "Peserta" + functions: + bynokartu: + methods: ["GET"] + path: "/peserta/:nokartu" + get_routes: "/nokartu/:nokartu" + # post_routes: "/Peserta/nokartu/:nokartu" + # put_routes: "/Peserta/nokartu/:nokartu" + # delete_routes: "/Peserta/nokartu/:nokartu" + get_path: "/Peserta/nokartu/:nokartu/tglSEP/:tglSEP" + # post_path: "/peserta" + # put_path: "/peserta/:nokartu" + # delete_path: "/peserta/:nokartu" + model: "PesertaRequest" + response_model: "PesertaResponse" + request_model: "RujukanRequest" + description: "Get participant eligibility information by card number" + summary: "Get Participant Info by No Kartu" + tags: ["Peserta"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 300 + + bynik: + methods: ["GET"] + path: "/peserta/nik/:nik" + get_routes: "/nik/:nik" + # post_routes: "/Peserta/nik/:nik" + # put_routes: "/Peserta/nik/:nik" + # delete_routes: "/Peserta/nik/:nik" + get_path: "/Peserta/nik/:nik/tglSEP/:tglSEP" + # post_path: "/peserta" + # put_path: "/peserta/nik/:nik" + # delete_path: "/peserta/nik/:nik" + model: "PesertaRequest" + response_model: "PesertaResponse" + request_model: "PesertaRequest" + description: "Get participant eligibility information by NIK" + summary: "Get Participant Info by NIK" + tags: ["Peserta"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 300 + + rujukan: + description: "Rujukan management endpoints" + handler_folder: "rujukan" + handler_file: "rujukan.go" + handler_name: "Rujukan" + functions: + rujukan: + methods: ["POST", "PUT", "DELETE"] + path: "/Rujukan" + # get_routes: "/Rujukan/:norujukan" + post_routes: "/Rujukan/:norujukan" + put_routes: "/Rujukan/:norujukan" + delete_routes: "/Rujukan/:norujukan" + # get_path: "/Rujukan/:norujukan" + post_path: "/Rujukan" + put_path: "/Rujukan/:norujukan" + delete_path: "/Rujukan/:norujukan" + model: "RujukanRequest" + response_model: "RujukanResponse" + request_model: "RujukanRequest" + description: "Manage rujukan" + summary: "Rujukan Management" + tags: ["Rujukan"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 180 + rujukanbalik: + methods: ["POST", "PUT", "DELETE"] + path: "/Rujukanbalik" + # get_routes: "/Rujukanbalik/:norujukan" + post_routes: "/Rujukanbalik/:norujukan" + put_routes: "/Rujukanbalik/:norujukan" + delete_routes: "/Rujukanbalik/:norujukan" + # get_path: "/Rujukanbalik/:norujukan" + post_path: "/Rujukanbalik" + put_path: "/Rujukanbalik/:norujukan" + delete_path: "/Rujukanbalik/:norujukan" + model: "RujukanRequest" + response_model: "RujukanResponse" + request_model: "RujukanRequest" + description: "Manage rujukan" + summary: "Rujukan Management" + tags: ["Rujukan"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 180 + + search: + description: "Search for rujukan endpoints" + handler_folder: "rujukan" + handler_file: "search.go" + handler_name: "Search" + functions: + bynorujukan: + methods: ["GET"] + path: "/Rujukan/:norujukan" + get_routes: "/bynorujukan/:norujukan" + # post_routes: "/bynorujukan/:norujukan" + # put_routes: "/bynorujukan/:norujukan" + # delete_routes: "/bynorujukan/:norujukan" + get_path: "/Rujukan/:norujukan" + # post_path: "/Rujukan" + # put_path: "/Rujukan/:norujukan" + # delete_path: "/Rujukan/:norujukan" + model: "RujukanRequest" + response_model: "RujukanResponse" + request_model: "RujukanRequest" + description: "Get rujukan by nomor rujukan" + summary: "Rujukan Management" + tags: ["Rujukan"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 300 + + bynokartu: + methods: ["GET"] + path: "/Rujukan/:nokartu" + get_routes: "/bynokartu/:nokartu" + # post_routes: "/bynokartu/:nokartu" + # put_routes: "/bynokartu/:nokartu" + # delete_routes: "/bynokartu/:nokartu" + get_path: "/Rujukan/:nokartu" + # post_path: "/Rujukan" + # put_path: "/Rujukan/:nokartu" + # delete_path: "/Rujukan/:nokartu" + model: "RujukanRequest" + response_model: "RujukanResponse" + request_model: "RujukanRequest" + description: "Get rujukan by card number" + summary: "Rujukan Management" + tags: ["Rujukan"] + require_auth: true + cache_enabled: true + enable_database: true # jika menggunakan database + cache_ttl: 300 + diff --git a/tools/general/generate-handler.go b/tools/general/generate-handler.go index bddbdcb..bd0408c 100644 --- a/tools/general/generate-handler.go +++ b/tools/general/generate-handler.go @@ -1,11 +1,15 @@ package main import ( + "flag" "fmt" + "log" "os" "path/filepath" "strings" "time" + + "gopkg.in/yaml.v2" ) // HandlerData contains template data for handler generation @@ -29,6 +33,83 @@ type HandlerData struct { Timestamp string } +// Config represents the YAML configuration structure +type Config struct { + Entities []Entity `yaml:"entities"` +} + +// Entity represents a single entity configuration in YAML +type Entity struct { + Name string `yaml:"name"` + Methods []string `yaml:"methods"` + Category string `yaml:"category,omitempty"` +} + +// ServicesConfig represents the new services-based YAML configuration structure +type ServicesConfig struct { + Global GlobalConfig `yaml:"global"` + Services map[string]ServiceConfig `yaml:"services"` +} + +// GlobalConfig represents global configuration +type GlobalConfig struct { + ModuleName string `yaml:"module_name"` + OutputDir string `yaml:"output_dir"` + EnableSwagger bool `yaml:"enable_swagger"` + EnableLogging bool `yaml:"enable_logging"` +} + +// ServiceConfig represents a service configuration +type ServiceConfig struct { + Name string `yaml:"name"` + Category string `yaml:"category"` + Package string `yaml:"package"` + Description string `yaml:"description"` + BaseURL string `yaml:"base_url"` + Timeout int `yaml:"timeout"` + RetryCount int `yaml:"retry_count"` + Endpoints map[string]EndpointConfig `yaml:"endpoints"` +} + +// EndpointConfig represents an endpoint configuration +type EndpointConfig struct { + Description string `yaml:"description"` + HandlerFolder string `yaml:"handler_folder"` + HandlerFile string `yaml:"handler_file"` + HandlerName string `yaml:"handler_name"` + TableName string `yaml:"table_name,omitempty"` + Functions map[string]FunctionConfig `yaml:"functions"` +} + +// FunctionConfig represents a function configuration +type FunctionConfig struct { + Methods []string `yaml:"methods"` + Path string `yaml:"path"` + GetRoutes string `yaml:"get_routes,omitempty"` + PostRoutes string `yaml:"post_routes,omitempty"` + PutRoutes string `yaml:"put_routes,omitempty"` + DeleteRoutes string `yaml:"delete_routes,omitempty"` + GetPath string `yaml:"get_path,omitempty"` + PostPath string `yaml:"post_path,omitempty"` + PutPath string `yaml:"put_path,omitempty"` + DeletePath string `yaml:"delete_path,omitempty"` + Model string `yaml:"model"` + ResponseModel string `yaml:"response_model"` + RequestModel string `yaml:"request_model,omitempty"` + Description string `yaml:"description"` + Summary string `yaml:"summary"` + Tags []string `yaml:"tags"` + RequireAuth bool `yaml:"require_auth"` + CacheEnabled bool `yaml:"cache_enabled"` + EnableDatabase bool `yaml:"enable_database"` + CacheTTL int `yaml:"cache_ttl"` + HasPagination bool `yaml:"has_pagination,omitempty"` + HasFilter bool `yaml:"has_filter,omitempty"` + HasSearch bool `yaml:"has_search,omitempty"` + HasStats bool `yaml:"has_stats,omitempty"` + HasDynamic bool `yaml:"has_dynamic,omitempty"` +} + type PathInfo struct { Category string EntityName string @@ -36,25 +117,47 @@ type PathInfo struct { FilePath string } -// parseEntityPath - Logic parsing yang diperbaiki +// Global variables for command line flags +var ( + forceFlag = flag.Bool("force", false, "Force overwrite existing files") + verboseFlag = flag.Bool("verbose", false, "Enable verbose output") + configFlag = flag.String("config", "", "Specify config file path") +) + +// Global file skip function +var shouldSkipExistingFile func(filePath string, fileType string) bool + +// parseEntityPath - Enhanced logic parsing dengan validasi lebih baik func parseEntityPath(entityPath string) (*PathInfo, error) { if strings.TrimSpace(entityPath) == "" { return nil, fmt.Errorf("entity path cannot be empty") } + var pathInfo PathInfo - parts := strings.Split(entityPath, "/") + + // Clean path untuk menghapus leading/trailing slashes + cleanedPath := strings.Trim(entityPath, "/") + parts := strings.Split(cleanedPath, "/") + // Validasi minimal 1 bagian (file saja) dan maksimal 4 if len(parts) < 1 || len(parts) > 4 { return nil, fmt.Errorf("invalid path format: use up to 4 levels like 'level1/level2/level3/entity'") } - // Validasi bagian kosong + + // Validasi bagian kosong dan karakter tidak valid for i, part := range parts { if strings.TrimSpace(part) == "" { return nil, fmt.Errorf("empty path segment at position %d", i+1) } + + // Validasi karakter untuk keamanan + if !isValidPathSegment(part) { + return nil, fmt.Errorf("invalid characters in path segment '%s' at position %d", part, i+1) + } } pathInfo.EntityName = parts[len(parts)-1] + if len(parts) > 1 { pathInfo.Category = parts[len(parts)-2] pathInfo.DirPath = strings.Join(parts[:len(parts)-1], "/") @@ -64,9 +167,24 @@ func parseEntityPath(entityPath string) (*PathInfo, error) { pathInfo.DirPath = "" pathInfo.FilePath = strings.ToLower(pathInfo.EntityName) + ".go" } + return &pathInfo, nil } +// Validasi karakter untuk path segment +func isValidPathSegment(segment string) bool { + // Hanya izinkan alphanumeric, underscore, dan dash + for _, char := range segment { + if !((char >= 'a' && char <= 'z') || + (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || + char == '_' || char == '-') { + return false + } + } + return true +} + // validateMethods - Validasi method yang diinput func validateMethods(methods []string) error { validMethods := map[string]bool{ @@ -94,23 +212,29 @@ func generateTableName(pathInfo *PathInfo) string { return "data_" + entityLower } -// createDirectories - Buat direktori sesuai struktur path +// createDirectories - Enhanced directory creation dengan better error handling func createDirectories(pathInfo *PathInfo) (string, string, error) { var handlerDir, modelDir string + // Support nested directories lebih baik if pathInfo.DirPath != "" { - handlerDir = filepath.Join("internal", "handlers", pathInfo.DirPath) - modelDir = filepath.Join("internal", "models", pathInfo.DirPath) + // Normalize path untuk memastikan konsistensi + normalizedPath := filepath.Clean(pathInfo.DirPath) + handlerDir = filepath.Join("internal", "handlers", normalizedPath) + modelDir = filepath.Join("internal", "models", normalizedPath) } else { handlerDir = filepath.Join("internal", "handlers") modelDir = filepath.Join("internal", "models") } - // Create directories + // Create directories dengan permission yang tepat for _, dir := range []string{handlerDir, modelDir} { if err := os.MkdirAll(dir, 0755); err != nil { return "", "", fmt.Errorf("failed to create directory %s: %v", dir, err) } + if *verboseFlag { + fmt.Printf("📁 Created directory: %s\n", dir) + } } return handlerDir, modelDir, nil @@ -140,8 +264,279 @@ func setMethods(data *HandlerData, methods []string) { } } +// Helper function untuk remove duplicate methods +func removeDuplicateMethods(methods []string) []string { + seen := make(map[string]bool) + result := []string{} + + for _, method := range methods { + lowerMethod := strings.ToLower(method) + if !seen[lowerMethod] { + seen[lowerMethod] = true + result = append(result, method) + } + } + + return result +} + +// Validasi apakah file sudah ada dan harus di-skip +func defaultShouldSkipExistingFile(filePath string, fileType string) bool { + if *forceFlag { + if *verboseFlag { + fmt.Printf("🔄 Force mode enabled, overwriting: %s\n", filePath) + } + return false + } + + if _, err := os.Stat(filePath); err == nil { + fmt.Printf("⚠️ %s file already exists: %s\n", strings.Title(fileType), filePath) + if *verboseFlag { + fmt.Printf(" Use --force to overwrite\n") + } + return true + } + return false +} + +func loadConfig(configPath string) (*Config, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML config: %w", err) + } + + return &config, nil +} + +func loadServicesConfig(configPath string) (*ServicesConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read services config file: %w", err) + } + + var config ServicesConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML services config: %w", err) + } + + return &config, nil +} + +// generateFromServicesConfig - RESTRUCTURED untuk agreggasi methods +func generateFromServicesConfig(config *ServicesConfig) { + for serviceName, service := range config.Services { + if *verboseFlag { + fmt.Printf("🔧 Processing service: %s\n", serviceName) + } + + for endpointName, endpoint := range service.Endpoints { + if *verboseFlag { + fmt.Printf(" 📍 Processing endpoint: %s\n", endpointName) + } + + // Parse entity path dari endpoint name + pathInfo, err := parseEntityPath(endpointName) + if err != nil { + logError(fmt.Sprintf("Error parsing entity path '%s'", endpointName), err, *verboseFlag) + continue + } + + // Override category dari service config + if service.Category != "" { + pathInfo.Category = service.Category + } + + // Set directory path dari handler_folder jika specified + if endpoint.HandlerFolder != "" { + pathInfo.DirPath = endpoint.HandlerFolder + } + + // AGGREGATE semua methods dari semua functions + var allMethods []string + var functionConfigs []FunctionConfig + + for functionName, function := range endpoint.Functions { + if *verboseFlag { + fmt.Printf(" ⚙️ Processing function: %s\n", functionName) + } + + // Tambahkan methods dari function ini + allMethods = append(allMethods, function.Methods...) + functionConfigs = append(functionConfigs, function) + } + + // Remove duplicates dari methods + allMethods = removeDuplicateMethods(allMethods) + + // Jika tidak ada methods, gunakan default + if len(allMethods) == 0 { + allMethods = []string{"get", "post", "put", "delete", "dynamic", "search"} + } + + // Validate methods + if err := validateMethods(allMethods); err != nil { + logError(fmt.Sprintf("Invalid methods for endpoint '%s'", endpointName), err, *verboseFlag) + continue + } + + // Override table name jika specified + tableName := endpoint.TableName + if tableName == "" { + tableName = generateTableName(pathInfo) + } + + // Generate handler data dengan service-specific information + entityName := strings.Title(pathInfo.EntityName) + entityLower := strings.ToLower(pathInfo.EntityName) + entityPlural := entityLower + "s" + + data := HandlerData{ + Name: entityName, + NameLower: entityLower, + NamePlural: entityPlural, + Category: pathInfo.Category, + DirPath: pathInfo.DirPath, + ModuleName: config.Global.ModuleName, + TableName: tableName, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Set methods berdasarkan aggregated methods + setMethods(&data, allMethods) + + // Set flags berdasarkan function configs + for _, function := range functionConfigs { + if function.HasPagination { + data.HasPagination = true + } + if function.HasFilter { + data.HasFilter = true + } + if function.HasSearch { + data.HasSearch = true + } + if function.HasStats { + data.HasStats = true + } + if function.HasDynamic { + data.HasDynamic = true + } + } + + // Create directories + handlerDir, modelDir, err := createDirectories(pathInfo) + if err != nil { + logError("Error creating directories", err, *verboseFlag) + continue + } + + // CHECK existing files sebelum generate + handlerPath := filepath.Join(handlerDir, entityLower+".go") + modelPath := filepath.Join(modelDir, entityLower+".go") + + if shouldSkipExistingFile(handlerPath, "handler") { + fmt.Printf("⚠️ Skipping handler generation: %s\n", handlerPath) + continue + } + + if shouldSkipExistingFile(modelPath, "model") { + fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath) + continue + } + + // Generate files (SEKALI SAJA per endpoint) + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) + + // HANYA UPDATE ROUTES SEKALI PER ENDPOINT setelah semua fungsi di-aggregate + updateRoutesFile(data) + + // Success output + logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), + fmt.Sprintf("Category: %s", pathInfo.Category), + fmt.Sprintf("Path: %s", pathInfo.DirPath), + fmt.Sprintf("Handler: %s", handlerPath), + fmt.Sprintf("Model: %s", modelPath), + fmt.Sprintf("Table: %s", tableName), + fmt.Sprintf("Methods: %s", strings.Join(allMethods, ", ")), + ) + } + } +} + func main() { - // Validasi argument + // Parse command line flags + flag.Parse() + + // Set global file skip function + shouldSkipExistingFile = defaultShouldSkipExistingFile + + // Determine config file path + configPath := "services-config.yaml" + if *configFlag != "" { + configPath = *configFlag + } + + // Check for services-config.yaml first (new format) + servicesConfig, err := loadServicesConfig(configPath) + if err == nil { + // Use services config + if *verboseFlag { + fmt.Printf("📄 Using services configuration from %s\n", configPath) + } + generateFromServicesConfig(servicesConfig) + return + } + + // Fallback to old config-handler.yml + oldConfigPath := "config-handler.yml" + if *configFlag == "" { + oldConfigPath = "config-handler.yml" + } + + config, err := loadConfig(oldConfigPath) + if err == nil { + // Generate from old config + if *verboseFlag { + fmt.Printf("📄 Using legacy configuration from %s\n", oldConfigPath) + } + for _, entity := range config.Entities { + pathInfo, err := parseEntityPath(entity.Name) + if err != nil { + logError(fmt.Sprintf("Error parsing entity path '%s'", entity.Name), err, *verboseFlag) + continue + } + + // Override category if specified in config + if entity.Category != "" { + pathInfo.Category = entity.Category + } + + // Use methods from config or default + methods := entity.Methods + if len(methods) == 0 { + methods = []string{"get", "post", "put", "delete", "dynamic", "search"} + } + + // Validate methods + if err := validateMethods(methods); err != nil { + logError(fmt.Sprintf("Invalid methods for entity '%s'", entity.Name), err, *verboseFlag) + continue + } + + generateForEntity(pathInfo, methods) + } + return + } + + // No config files found, fallback to command line arguments + fmt.Printf("⚠️ No config files found (%s or %s), falling back to command line arguments\n", configPath, oldConfigPath) + if len(os.Args) < 2 { fmt.Println("Usage: go run generate-handler.go [path/]entity [methods]") fmt.Println("Examples:") @@ -149,6 +544,11 @@ func main() { fmt.Println(" go run generate-handler.go retribusi/tarif get post put delete dynamic search") fmt.Println(" go run generate-handler.go product/category/subcategory/item get post") fmt.Println("\nSupported methods: get, post, put, delete, stats, dynamic, search") + fmt.Println("\nAlternatively, create a services-config.yaml or config-handler.yml file with configurations.") + fmt.Println("\nFlags:") + fmt.Println(" --force Force overwrite existing files") + fmt.Println(" --verbose Enable verbose output") + fmt.Println(" --config Specify config file path") os.Exit(1) } @@ -156,7 +556,7 @@ func main() { entityPath := strings.TrimSpace(os.Args[1]) pathInfo, err := parseEntityPath(entityPath) if err != nil { - fmt.Printf("❌ Error parsing path: %v\n", err) + logError("Error parsing path", err, true) os.Exit(1) } @@ -171,10 +571,15 @@ func main() { // Validate methods if err := validateMethods(methods); err != nil { - fmt.Printf("❌ %v\n", err) + logError("Method validation failed", err, true) os.Exit(1) } + // Generate for single entity + generateForEntity(pathInfo, methods) +} + +func generateForEntity(pathInfo *PathInfo, methods []string) { // Format names entityName := strings.Title(pathInfo.EntityName) // PascalCase entity name entityLower := strings.ToLower(pathInfo.EntityName) @@ -203,39 +608,42 @@ func main() { // Create directories handlerDir, modelDir, err := createDirectories(pathInfo) if err != nil { - fmt.Printf("❌ Error creating directories: %v\n", err) - os.Exit(1) + logError("Error creating directories", err, *verboseFlag) + return + } + + // CHECK existing files sebelum generate + handlerPath := filepath.Join(handlerDir, entityLower+".go") + modelPath := filepath.Join(modelDir, entityLower+".go") + + if shouldSkipExistingFile(handlerPath, "handler") { + fmt.Printf("⚠️ Skipping handler generation: %s\n", handlerPath) + return + } + + if shouldSkipExistingFile(modelPath, "model") { + fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath) + return } // Generate files generateHandlerFile(data, handlerDir) generateModelFile(data, modelDir) - updateRoutesFile(data) + updateRoutesFile(data) // Ini untuk legacy mode, masih ok karena hanya sekali per entity // Success output - fmt.Printf("✅ Successfully generated handler: %s\n", entityName) - if pathInfo.Category != "" { - fmt.Printf("📁 Category: %s\n", pathInfo.Category) - } - if pathInfo.DirPath != "" { - fmt.Printf("📂 Path: %s\n", pathInfo.DirPath) - } - fmt.Printf("📄 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) - fmt.Printf("📄 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) - fmt.Printf("🗄️ Table: %s\n", tableName) - fmt.Printf("🛠️ Methods: %s\n", strings.Join(methods, ", ")) + logSuccess(fmt.Sprintf("Successfully generated handler: %s", entityName), + fmt.Sprintf("Category: %s", pathInfo.Category), + fmt.Sprintf("Path: %s", pathInfo.DirPath), + fmt.Sprintf("Handler: %s", handlerPath), + fmt.Sprintf("Model: %s", modelPath), + fmt.Sprintf("Table: %s", tableName), + fmt.Sprintf("Methods: %s", strings.Join(methods, ", ")), + ) } // ================= HANDLER GENERATION ===================== func generateHandlerFile(data HandlerData, handlerDir string) { - // var modelsImportPath string - // if data.Category != "" { - // modelsImportPath = data.ModuleName + "/internal/models/" + data.Category - // } else { - // modelsImportPath = data.ModuleName + "/internal/models" - // } - - // pakai strings.Builder biar lebih clean var handlerContent strings.Builder // Header @@ -245,15 +653,19 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(` "` + data.ModuleName + `/internal/database"` + "\n") handlerContent.WriteString(` models "` + data.ModuleName + `/internal/models"` + "\n") if data.Category != "models" { - handlerContent.WriteString(` "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") + handlerContent.WriteString(data.Category + `Models "` + data.ModuleName + `/internal/models/` + data.Category + `"` + "\n") } - // Conditional imports + // Conditional imports based on enabled methods if data.HasDynamic || data.HasSearch { handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n") } - handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\n") + // Only import validation if POST is enabled (since validation is primarily for create operations) + if data.HasPost { + handlerContent.WriteString(` "` + data.ModuleName + `/internal/utils/validation"` + "\n") + } + handlerContent.WriteString(` "context"` + "\n") handlerContent.WriteString(` "database/sql"` + "\n") handlerContent.WriteString(` "fmt"` + "\n") @@ -281,18 +693,25 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(" " + data.NameLower + "once.Do(func() {\n") handlerContent.WriteString(" " + data.NameLower + "db = database.New(config.LoadConfig())\n") handlerContent.WriteString(" " + data.NameLower + "validate = validator.New()\n") - handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") + + // Only register validation if POST is enabled + if data.HasPost { + handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") + } + handlerContent.WriteString(" if " + data.NameLower + "db == nil {\n") handlerContent.WriteString(" log.Fatal(\"Failed to initialize database connection\")\n") handlerContent.WriteString(" }\n") handlerContent.WriteString(" })\n") handlerContent.WriteString("}\n\n") - // Custom validation - handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n") - handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n") - handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n") - handlerContent.WriteString("}\n\n") + // Custom validation - Only include if POST is enabled + if data.HasPost { + handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n") + handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n") + handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n") + handlerContent.WriteString("}\n\n") + } // Handler struct handlerContent.WriteString("// " + data.Name + "Handler handles " + data.NameLower + " services\n") @@ -308,7 +727,7 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(" }\n") handlerContent.WriteString("}\n") - // Add optional methods + // Add optional methods based on enabled flags if data.HasGet { handlerContent.WriteString(generateGetMethods(data)) } @@ -331,7 +750,7 @@ func generateHandlerFile(data HandlerData, handlerDir string) { handlerContent.WriteString(generateStatsMethod(data)) } - // Add helper methods + // Add helper methods - this function now handles conditional generation internally handlerContent.WriteString(generateHelperMethods(data)) // Write into file @@ -352,109 +771,109 @@ func generateGetMethods(data HandlerData) string { // @Param include_summary query bool false "Include aggregation summary" default(false) // @Param status query string false "Filter by status" // @Param search query string false "Search in multiple fields" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetResponse "Success response" +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NamePlural + ` [get] func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { - // Parse pagination parameters - limit, offset, err := h.parsePaginationParams(c) - if err != nil { - h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) - return - } + // Parse pagination parameters + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } - // Parse filter parameters - filter := h.parseFilterParams(c) - includeAggregation := c.Query("include_summary") == "true" - - // Get database connection - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + // Parse filter parameters + filter := h.parseFilterParams(c) + includeAggregation := c.Query("include_summary") == "true" + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - // Create context with timeout - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() - // Execute concurrent operations - var ( - items []` + data.Category + `.` + data.Name + ` - total int - aggregateData *models.AggregateData - wg sync.WaitGroup - errChan = make(chan error, 3) - mu sync.Mutex - ) + // Execute concurrent operations + var ( + items []` + data.Category + `Models.` + data.Name + ` + total int + aggregateData *models.AggregateData + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex + ) - // Fetch total count - wg.Add(1) - go func() { - defer wg.Done() - if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { - mu.Lock() - errChan <- fmt.Errorf("failed to get total count: %w", err) - mu.Unlock() - } - }() + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + errChan <- fmt.Errorf("failed to get total count: %w", err) + mu.Unlock() + } + }() - // Fetch main data - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to fetch data: %w", err) - } else { - items = result - } - mu.Unlock() - }() + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetch` + data.Name + `s(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to fetch data: %w", err) + } else { + items = result + } + mu.Unlock() + }() - // Fetch aggregation data if requested - if includeAggregation { - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.getAggregateData(ctx, dbConn, filter) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to get aggregate data: %w", err) - } else { - aggregateData = result - } - mu.Unlock() - }() - } + // Fetch aggregation data if requested + if includeAggregation { + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.getAggregateData(ctx, dbConn, filter) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to get aggregate data: %w", err) + } else { + aggregateData = result + } + mu.Unlock() + }() + } - // Wait for all goroutines - wg.Wait() - close(errChan) + // Wait for all goroutines + wg.Wait() + close(errChan) - // Check for errors - for err := range errChan { - if err != nil { - h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) - return - } - } + // Check for errors + for err := range errChan { + if err != nil { + h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) + return + } + } - // Build response - meta := h.calculateMeta(limit, offset, total) - response := ` + data.Category + `.` + data.Name + `GetResponse{ - Message: "Data ` + data.Category + ` berhasil diambil", - Data: items, - Meta: meta, - } + // Build response + meta := h.calculateMeta(limit, offset, total) + response := ` + data.Category + `Models.` + data.Name + `GetResponse{ + Message: "Data ` + data.Category + ` berhasil diambil", + Data: items, + Meta: meta, + } - if includeAggregation && aggregateData != nil { - response.Summary = aggregateData - } + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) } // Get` + data.Name + `ByID godoc @@ -464,45 +883,45 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetByIDResponse "Success response" +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetByIDResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NameLower + `/{id} [get] func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() - item, err := h.get` + data.Name + `ByID(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError) - } - return - } + item, err := h.get` + data.Name + `ByID(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to get ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } - response := ` + data.Category + `.` + data.Name + `GetByIDResponse{ - Message: "` + data.Category + ` details retrieved successfully", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `GetByIDResponse{ + Message: "` + data.Category + ` details retrieved successfully", + Data: item, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -520,127 +939,276 @@ func generateDynamicMethod(data HandlerData) string { // @Param sort query string false "Sort fields (e.g., sort=date_created,-name)" // @Param limit query int false "Limit" default(10) // @Param offset query int false "Offset" default(0) -// @Success 200 {object} ` + data.Category + `.` + data.Name + `GetResponse "Success response" +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NamePlural + `/dynamic [get] func (h *` + data.Name + `Handler) Get` + data.Name + `Dynamic(c *gin.Context) { - // Parse query parameters - parser := utils.NewQueryParser().SetLimits(10, 100) - dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query()) - if err != nil { - h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest) - return - } + // Parse query parameters + parser := utils.NewQueryParser().SetLimits(10, 100) + dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query()) + if err != nil { + h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest) + return + } - // Get database connection - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - // Create context with timeout - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() - // Execute query with dynamic filtering - items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery) - if err != nil { - h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) - return - } + // Execute query with dynamic filtering + items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, dynamicQuery) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } - // Build response - meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total) - response := ` + data.Category + `.` + data.Name + `GetResponse{ - Message: "Data ` + data.Category + ` berhasil diambil", - Data: items, - Meta: meta, - } + // Build response + meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total) + response := ` + data.Category + `Models.` + data.Name + `GetResponse{ + Message: "Data ` + data.Category + ` berhasil diambil", + Data: items, + Meta: meta, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } func generateSearchMethod(data HandlerData) string { return ` -// Search` + data.Name + `Advanced provides advanced search capabilities +// Get` + data.Name + `Search godoc +// @Summary Get ` + data.NameLower + ` with Search filtering +// @Description Returns ` + data.NamePlural + ` with advanced dynamic filtering like Directus +// @Tags ` + data.Name + ` +// @Accept json +// @Produce json +// @Param fields query string false "Fields to select (e.g., fields=*.*)" +// @Param filter[column][operator] query string false "Search filters (e.g., filter[name][_eq]=value)" +// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `GetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/` + data.NamePlural + `/search [get] func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) { - // Parse complex search parameters - searchQuery := c.Query("q") - if searchQuery == "" { - h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest) - return - } + // Parse complex search parameters + searchQuery := c.Query("q") + if searchQuery == "" { + // If no search query provided, return all records with default sorting + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{}, // Empty filters - fetch` + data.Name + `sDynamic will add default deleted filter + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } - // Build dynamic query for search - query := utils.DynamicQuery{ - Fields: []string{"*"}, - Filters: []utils.FilterGroup{{ - Filters: []utils.DynamicFilter{ - { - Column: "status", - Operator: utils.OpNotEqual, - Value: "deleted", - }, - { - Column: "name", - Operator: utils.OpContains, - Value: searchQuery, - LogicOp: "OR", - }, - }, + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute query to get all records + ` + data.NameLower + `s, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := ` + data.Category + `Models.` + data.Name + `GetResponse{ + Message: "All records retrieved (no search query provided)", + Data: ` + data.NameLower + `s, + Meta: meta, + } + + c.JSON(http.StatusOK, response) + return + } + + // Build dynamic query for search + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{ + { + Column: "name", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + }, + LogicOp: "AND", + }}, + Sort: []utils.SortField{{ + Column: "date_created", + Order: "DESC", + }}, + Limit: 20, + Offset: 0, + } + + // Parse pagination if provided + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { + query.Limit = l + } + } + + if offset := c.Query("offset"); offset != "" { + if o, err := strconv.Atoi(offset); err == nil && o >= 0 { + query.Offset = o + } + } + + // Get database connection + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute search + ` + data.NameLower + `s, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := ` + data.Category + `Models.` + data.Name + `GetResponse{ + Message: fmt.Sprintf("Search results for '%s'", searchQuery), + Data: ` + data.NameLower + `s, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} +// fetch` + data.Name + `sDynamic executes dynamic query +func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) { + // Setup query builders + countBuilder := utils.NewQueryBuilder("` + data.TableName + `"). + SetColumnMapping(map[string]string{ + // Add your column mappings here + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + // Add other allowed columns here + }) + + mainBuilder := utils.NewQueryBuilder("` + data.TableName + `"). + SetColumnMapping(map[string]string{ + // Add your column mappings here + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + // Add other allowed columns here + }) + + // Add default filter to exclude deleted records + if len(query.Filters) > 0 { + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, LogicOp: "AND", - }}, - Sort: []utils.SortField{{ - Column: "date_created", - Order: "DESC", - }}, - Limit: 20, - Offset: 0, + }}, query.Filters...) + } else { + query.Filters = []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }} } - // Parse pagination if provided - if limit := c.Query("limit"); limit != "" { - if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { - query.Limit = l - } - } - if offset := c.Query("offset"); offset != "" { - if o, err := strconv.Atoi(offset); err == nil && o >= 0 { - query.Offset = o - } - } + // Execute queries sequentially + var total int + var items []` + data.Category + `Models.` + data.Name + ` - // Get database connection - dbConn, err := h.db.GetDB("postgres_satudata") + // 1. Get total count + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + + countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return + return nil, 0, fmt.Errorf("failed to build count query: %w", err) } - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } - // Execute search - items, total, err := h.fetch` + data.Name + `sDynamic(ctx, dbConn, query) + // 2. Get main data + mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) if err != nil { - h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) - return + return nil, 0, fmt.Errorf("failed to build main query: %w", err) } - // Build response - meta := h.calculateMeta(query.Limit, query.Offset, total) - response := ` + data.Category + `.` + data.Name + `GetResponse{ - Message: fmt.Sprintf("Search results for '%s'", searchQuery), - Data: items, - Meta: meta, + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + return nil, 0, fmt.Errorf("failed to execute main query: %w", err) + } + defer rows.Close() + + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) + } + items = append(items, item) } - c.JSON(http.StatusOK, response) -}` + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows iteration error: %w", err) + } + + return items, total, nil +} +` } func generateCreateMethod(data HandlerData) string { @@ -652,51 +1220,51 @@ func generateCreateMethod(data HandlerData) string { // @Tags ` + data.Name + ` // @Accept json // @Produce json -// @Param request body ` + data.Category + `.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" -// @Success 201 {object} ` + data.Category + `.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" +// @Param request body ` + data.Category + `Models.` + data.Name + `CreateRequest true "` + data.Name + ` creation request" +// @Success 201 {object} ` + data.Category + `Models.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NamePlural + ` [post] func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { - var req ` + data.Category + `.` + data.Name + `CreateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } + var req ` + data.Category + `Models.` + data.Name + `CreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } - // Validate request - if err := ` + data.NameLower + `validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } + // Validate request + if err := ` + data.NameLower + `validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() - // Validate duplicate and daily submission - if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } + // Validate duplicate and daily submission + if err := h.validate` + data.Name + `Submission(ctx, dbConn, &req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } - item, err := h.create` + data.Name + `(ctx, dbConn, &req) - if err != nil { - h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError) - return - } + item, err := h.create` + data.Name + `(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create ` + data.NameLower + `", err, http.StatusInternalServerError) + return + } - response := ` + data.Category + `.` + data.Name + `CreateResponse{ - Message: "` + data.Name + ` berhasil dibuat", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `CreateResponse{ + Message: "` + data.Name + ` berhasil dibuat", + Data: item, + } - c.JSON(http.StatusCreated, response) + c.JSON(http.StatusCreated, response) }` } @@ -710,61 +1278,61 @@ func generateUpdateMethod(data HandlerData) string { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Param request body ` + data.Category + `.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully" +// @Param request body ` + data.Category + `Models.` + data.Name + `UpdateRequest true "` + data.Name + ` update request" +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `UpdateResponse "` + data.Name + ` updated successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NameLower + `/{id} [put] func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } - var req ` + data.Category + `.` + data.Name + `UpdateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } + var req ` + data.Category + `Models.` + data.Name + `UpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } - // Set ID from path parameter - req.ID = id + // Set ID from path parameter + req.ID = id - // Validate request - if err := ` + data.NameLower + `validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } + // Validate request + if err := ` + data.NameLower + `validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() - item, err := h.update` + data.Name + `(ctx, dbConn, &req) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError) - } - return - } + item, err := h.update` + data.Name + `(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } - response := ` + data.Category + `.` + data.Name + `UpdateResponse{ - Message: "` + data.Name + ` berhasil diperbarui", - Data: item, - } + response := ` + data.Category + `Models.` + data.Name + `UpdateResponse{ + Message: "` + data.Name + ` berhasil diperbarui", + Data: item, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -778,45 +1346,45 @@ func generateDeleteMethod(data HandlerData) string { // @Accept json // @Produce json // @Param id path string true "` + data.Name + ` ID (UUID)" -// @Success 200 {object} ` + data.Category + `.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully" +// @Success 200 {object} ` + data.Category + `Models.` + data.Name + `DeleteResponse "` + data.Name + ` deleted successfully" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NameLower + `/{id} [delete] func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() - err = h.delete` + data.Name + `(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError) - } - return - } + err = h.delete` + data.Name + `(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "` + data.Name + ` not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to delete ` + data.NameLower + `", err, http.StatusInternalServerError) + } + return + } - response := ` + data.Category + `.` + data.Name + `DeleteResponse{ - Message: "` + data.Name + ` berhasil dihapus", - ID: id, - } + response := ` + data.Category + `Models.` + data.Name + `DeleteResponse{ + Message: "` + data.Name + ` berhasil dihapus", + ID: id, + } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, response) }` } @@ -834,573 +1402,636 @@ func generateStatsMethod(data HandlerData) string { // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/` + data.NamePlural + `/stats [get] func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { - dbConn, err := h.db.GetDB("postgres_satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } + dbConn, err := h.db.GetDB("postgres_satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() - filter := h.parseFilterParams(c) - aggregateData, err := h.getAggregateData(ctx, dbConn, filter) - if err != nil { - h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) - return - } + filter := h.parseFilterParams(c) + aggregateData, err := h.getAggregateData(ctx, dbConn, filter) + if err != nil { + h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) + return + } - c.JSON(http.StatusOK, gin.H{ - "message": "Statistik ` + data.NameLower + ` berhasil diambil", - "data": aggregateData, - }) + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik ` + data.NameLower + ` berhasil diambil", + "data": aggregateData, + }) }` } func generateHelperMethods(data HandlerData) string { - helperMethods := ` + var helperMethods strings.Builder -// Database operations -func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `.` + data.Name + `, error) { - query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" - row := dbConn.QueryRowContext(ctx, query, id) - - var item ` + data.Category + `.` + data.Name + ` - err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, err - } - - return &item, nil + // Helper methods yang selalu dibutuhkan untuk semua handlers + helperMethods.WriteString(` + +// Optimized scanning function - selalu dibutuhkan untuk semua operasi database +func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `Models.` + data.Name + `, error) { + var item ` + data.Category + `Models.` + data.Name + ` + + // Scan into individual fields to handle nullable types properly + err := rows.Scan( + &item.ID, + &item.Status, + &item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32 + &item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString + &item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime + &item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString + &item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime + &item.Name.String, &item.Name.Valid, // sql.NullString + ) + + return item, err } -func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) (*` + data.Category + `.` + data.Name + `, error) { - id := uuid.New().String() - now := time.Now() - - query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) - - var item ` + data.Category + `.` + data.Name + ` - err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err) - } - - return &item, nil -} - -func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `UpdateRequest) (*` + data.Category + `.` + data.Name + `, error) { - now := time.Now() - - query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) - - var item ` + data.Category + `.` + data.Name + ` - err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err) - } - - return &item, nil -} - -func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { - now := time.Now() - query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" - - result, err := dbConn.ExecContext(ctx, query, id, now) - if err != nil { - return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %w", err) - } - - if rowsAffected == 0 { - return sql.ErrNoRows - } - - return nil -} - -func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `.` + data.Name + `, error) { - whereClause, args := h.buildWhereClause(filter) - query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) - args = append(args, limit, offset) - - rows, err := dbConn.QueryContext(ctx, query, args...) - if err != nil { - return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err) - } - defer rows.Close() - - items := make([]` + data.Category + `.` + data.Name + `, 0, limit) - for rows.Next() { - item, err := h.scan` + data.Name + `(rows) - if err != nil { - return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err) - } - items = append(items, item) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("rows iteration error: %w", err) - } - - log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) - return items, nil -}` - - // Add dynamic fetch method if needed - if data.HasDynamic { - helperMethods += ` - -// fetchRetribusisDynamic executes dynamic query -func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `.` + data.Name + `, int, error) { - // Setup query builder - builder := utils.NewQueryBuilder("` + data.TableName + `"). - SetAllowedColumns([]string{ - "id", "status", "sort", "user_created", "date_created", - "user_updated", "date_updated", "name", - }) - - // Add default filter to exclude deleted records - query.Filters = append([]utils.FilterGroup{{ - Filters: []utils.DynamicFilter{{ - Column: "status", - Operator: utils.OpNotEqual, - Value: "deleted", - }}, - LogicOp: "AND", - }}, query.Filters...) - - // Execute concurrent queries - var ( - items [] ` + data.Category + `.` + data.Name + ` - total int - wg sync.WaitGroup - errChan = make(chan error, 2) - mu sync.Mutex - ) - - // Fetch total count - wg.Add(1) - go func() { - defer wg.Done() - countQuery := query - countQuery.Limit = 0 - countQuery.Offset = 0 - countSQL, countArgs, err := builder.BuildCountQuery(countQuery) - if err != nil { - errChan <- fmt.Errorf("failed to build count query: %w", err) - return - } - if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { - errChan <- fmt.Errorf("failed to get total count: %w", err) - return - } - }() - - // Fetch main data - wg.Add(1) - go func() { - defer wg.Done() - mainSQL, mainArgs, err := builder.BuildQuery(query) - if err != nil { - errChan <- fmt.Errorf("failed to build main query: %w", err) - return - } - - rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) - if err != nil { - errChan <- fmt.Errorf("failed to execute main query: %w", err) - return - } - defer rows.Close() - - var results []` + data.Category + `.` + data.Name + ` - for rows.Next() { - item, err := h.scan` + data.Name + `(rows) - if err != nil { - errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) - return - } - results = append(results, item) - } - - if err := rows.Err(); err != nil { - errChan <- fmt.Errorf("rows iteration error: %w", err) - return - } - - mu.Lock() - items = results - mu.Unlock() - }() - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - return nil, 0, err - } - } - - return items, total, nil -} -` - } - - helperMethods += ` -// Optimized scanning function -func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `.` + data.Name + `, error) { - var item ` + data.Category + `.` + data.Name + ` - - // Scan into individual fields to handle nullable types properly - err := rows.Scan( - &item.ID, - &item.Status, - &item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32 - &item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString - &item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime - &item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString - &item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime - &item.Name.String, &item.Name.Valid, // sql.NullString - ) - - return item, err -} - -func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter, total *int) error { - whereClause, args := h.buildWhereClause(filter) - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) - if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { - return fmt.Errorf("total count query failed: %w", err) - } - return nil -} - -// Get comprehensive aggregate data dengan filter support -func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `.` + data.Name + `Filter) (*models.AggregateData, error) { - aggregate := &models.AggregateData{ - ByStatus: make(map[string]int), - } - - // Build where clause untuk filter - whereClause, args := h.buildWhereClause(filter) - - // Use concurrent execution untuk performance - var wg sync.WaitGroup - var mu sync.Mutex - errChan := make(chan error, 4) - - // 1. Count by status - wg.Add(1) - go func() { - defer wg.Done() - statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) - - rows, err := dbConn.QueryContext(ctx, statusQuery, args...) - if err != nil { - errChan <- fmt.Errorf("status query failed: %w", err) - return - } - defer rows.Close() - - mu.Lock() - for rows.Next() { - var status string - var count int - if err := rows.Scan(&status, &count); err != nil { - mu.Unlock() - errChan <- fmt.Errorf("status scan failed: %w", err) - return - } - aggregate.ByStatus[status] = count - switch status { - case "active": - aggregate.TotalActive = count - case "draft": - aggregate.TotalDraft = count - case "inactive": - aggregate.TotalInactive = count - } - } - mu.Unlock() - - if err := rows.Err(); err != nil { - errChan <- fmt.Errorf("status iteration error: %w", err) - } - }() - - // 2. Get last updated time dan today statistics - wg.Add(1) - go func() { - defer wg.Done() - - // Last updated - lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause) - var lastUpdated sql.NullTime - if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { - errChan <- fmt.Errorf("last updated query failed: %w", err) - return - } - - // Today statistics - today := time.Now().Format("2006-01-02") - todayStatsQuery := fmt.Sprintf(` + "`" + ` - SELECT - SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, - SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today - FROM ` + data.TableName + ` - WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause) - - todayArgs := append(args, today) - var createdToday, updatedToday int - if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { - errChan <- fmt.Errorf("today stats query failed: %w", err) - return - } - - mu.Lock() - if lastUpdated.Valid { - aggregate.LastUpdated = &lastUpdated.Time - } - aggregate.CreatedToday = createdToday - aggregate.UpdatedToday = updatedToday - mu.Unlock() - }() - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - return nil, err - } - } - - return aggregate, nil -} - -// Enhanced error handling +// Enhanced error handling - selalu dibutuhkan untuk semua handlers func (h *` + data.Name + `Handler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { - log.Printf("[ERROR] %s: %v", message, err) - h.respondError(c, message, err, statusCode) + log.Printf("[ERROR] %s: %v", message, err) + h.respondError(c, message, err, statusCode) } func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, err error, statusCode int) { - errorMessage := message - if gin.Mode() == gin.ReleaseMode { - errorMessage = "Internal server error" - } - - c.JSON(statusCode, models.ErrorResponse{ - Error: errorMessage, - Code: statusCode, - Message: err.Error(), - Timestamp: time.Now(), - }) + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + + c.JSON(statusCode, models.ErrorResponse{ + Error: errorMessage, + Code: statusCode, + Message: err.Error(), + Timestamp: time.Now(), + }) } +`) + + // Helper methods untuk GET operations + if data.HasGet { + helperMethods.WriteString(` + +// Database operations untuk GET by ID +func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*` + data.Category + `Models.` + data.Name + `, error) { + query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" + row := dbConn.QueryRowContext(ctx, query, id) + + var item ` + data.Category + `Models.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, err + } + + return &item, nil +} +`) + + // Helper untuk fetch dengan pagination/filter + helperMethods.WriteString(` + +func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter, limit, offset int) ([]` + data.Category + `Models.` + data.Name + `, error) { + whereClause, args := h.buildWhereClause(filter) + query := fmt.Sprintf("SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) + args = append(args, limit, offset) + + rows, err := dbConn.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err) + } + defer rows.Close() + + items := make([]` + data.Category + `Models.` + data.Name + `, 0, limit) + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + return nil, fmt.Errorf("scan ` + data.Name + ` failed: %w", err) + } + items = append(items, item) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + if *verboseFlag { + log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) + } + return items, nil +} +`) + + // Helper untuk pagination + helperMethods.WriteString(` // Parse pagination parameters dengan validation yang lebih ketat func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) { - limit := 10 // Default limit - offset := 0 // Default offset - - if limitStr := c.Query("limit"); limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) - } - if parsedLimit <= 0 { - return 0, 0, fmt.Errorf("limit must be greater than 0") - } - if parsedLimit > 100 { - return 0, 0, fmt.Errorf("limit cannot exceed 100") - } - limit = parsedLimit - } - - if offsetStr := c.Query("offset"); offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) - } - if parsedOffset < 0 { - return 0, 0, fmt.Errorf("offset cannot be negative") - } - offset = parsedOffset - } - - log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) - return limit, offset, nil + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + if *verboseFlag { + log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + } + return limit, offset, nil } +`) -func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `.` + data.Name + `Filter { - filter := ` + data.Category + `.` + data.Name + `Filter{} - - if status := c.Query("status"); status != "" { - if models.IsValidStatus(status) { - filter.Status = &status - } - } - - if search := c.Query("search"); search != "" { - filter.Search = &search - } - - // Parse date filters - if dateFromStr := c.Query("date_from"); dateFromStr != "" { - if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { - filter.DateFrom = &dateFrom - } - } - - if dateToStr := c.Query("date_to"); dateToStr != "" { - if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { - filter.DateTo = &dateTo - } - } - - return filter + // Helper untuk filter (jika ada filter atau search) + if data.HasFilter || data.HasSearch { + helperMethods.WriteString(` + +func (h *` + data.Name + `Handler) parseFilterParams(c *gin.Context) ` + data.Category + `Models.` + data.Name + `Filter { + filter := ` + data.Category + `Models.` + data.Name + `Filter{} + + if status := c.Query("status"); status != "" { + if models.IsValidStatus(status) { + filter.Status = &status + } + } + + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse date filters + if dateFromStr := c.Query("date_from"); dateFromStr != "" { + if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + filter.DateFrom = &dateFrom + } + } + + if dateToStr := c.Query("date_to"); dateToStr != "" { + if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + filter.DateTo = &dateTo + } + } + + return filter } // Build WHERE clause dengan filter parameters -func (h *` + data.Name + `Handler) buildWhereClause(filter ` + data.Category + `.` + data.Name + `Filter) (string, []interface{}) { - conditions := []string{"status != 'deleted'"} - args := []interface{}{} - paramCount := 1 - - if filter.Status != nil { - conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) - args = append(args, *filter.Status) - paramCount++ - } - - if filter.Search != nil { - searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) - conditions = append(conditions, searchCondition) - searchTerm := "%" + *filter.Search + "%" - args = append(args, searchTerm) - paramCount++ - } - - if filter.DateFrom != nil { - conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) - args = append(args, *filter.DateFrom) - paramCount++ - } - - if filter.DateTo != nil { - conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) - args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) - paramCount++ - } - - return strings.Join(conditions, " AND "), args +func (h *` + data.Name + `Handler) buildWhereClause(filter ` + data.Category + `Models.` + data.Name + `Filter) (string, []interface{}) { + conditions := []string{"status != 'deleted'"} + args := []interface{}{} + paramCount := 1 + + if filter.Status != nil { + conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) + args = append(args, *filter.Status) + paramCount++ + } + + if filter.Search != nil { + searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) + conditions = append(conditions, searchCondition) + searchTerm := "%" + *filter.Search + "%" + args = append(args, searchTerm) + paramCount++ + } + + if filter.DateFrom != nil { + conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) + args = append(args, *filter.DateFrom) + paramCount++ + } + + if filter.DateTo != nil { + conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) + args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) + paramCount++ + } + + return strings.Join(conditions, " AND "), args } +`) + } + + // Helper untuk pagination meta + helperMethods.WriteString(` func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse { - totalPages := 0 - currentPage := 1 - if limit > 0 { - totalPages = (total + limit - 1) / limit // Ceiling division - currentPage = (offset / limit) + 1 - } - - return models.MetaResponse{ - Limit: limit, - Offset: offset, - Total: total, - TotalPages: totalPages, - CurrentPage: currentPage, - HasNext: offset+limit < total, - HasPrev: offset > 0, + totalPages := 0 + currentPage := 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit // Ceiling division + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} +`) + + // Helper untuk total count (dibutuhkan untuk pagination dan stats) + if data.HasPagination || data.HasStats { + helperMethods.WriteString(` + +func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { + return fmt.Errorf("total count query failed: %w", err) + } + return nil +} +`) + } + + // Helper untuk aggregate data (stats) + if data.HasStats { + helperMethods.WriteString(` + +// Get comprehensive aggregate data dengan filter support +func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `Models.` + data.Name + `Filter) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + } + + // Build where clause untuk filter + whereClause, args := h.buildWhereClause(filter) + + // Use concurrent execution untuk performance + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 4) + + // 1. Count by status + wg.Add(1) + go func() { + defer wg.Done() + statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) + + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var status string + var count int + if err := rows.Scan(&status, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("status scan failed: %w", err) + return + } + aggregate.ByStatus[status] = count + switch status { + case "active": + aggregate.TotalActive = count + case "draft": + aggregate.TotalDraft = count + case "inactive": + aggregate.TotalInactive = count + } + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("status iteration error: %w", err) + } + }() + + // 2. Get last updated time dan today statistics + wg.Add(1) + go func() { + defer wg.Done() + + // Last updated + lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM ` + data.TableName + ` WHERE %s AND date_updated IS NOT NULL", whereClause) + var lastUpdated sql.NullTime + if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + // Today statistics + today := time.Now().Format("2006-01-02") + todayStatsQuery := fmt.Sprintf(` + "`" + ` + SELECT + SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, + SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today + FROM ` + data.TableName + ` + WHERE %s` + "`" + `, len(args)+1, len(args)+1, len(args)+1, whereClause) + + todayArgs := append(args, today) + var createdToday, updatedToday int + if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { + errChan <- fmt.Errorf("today stats query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} +`) + } } + + // Helper methods untuk POST operations + if data.HasPost { + helperMethods.WriteString(` + +// Database operations untuk CREATE +func (h *` + data.Name + `Handler) create` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) (*` + data.Category + `Models.` + data.Name + `, error) { + id := uuid.New().String() + now := time.Now() + + query := "INSERT INTO ` + data.TableName + ` (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) + + var item ` + data.Category + `Models.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to create ` + data.NameLower + `: %w", err) + } + + return &item, nil } // validate` + data.Name + `Submission performs validation for duplicate entries and daily submission limits -func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) error { - // Import the validation utility - validator := validation.NewDuplicateValidator(dbConn) - - // Use default configuration - config := validation.ValidationConfig{ - TableName: "` + data.TableName + `", - IDColumn: "id", - StatusColumn: "status", - DateColumn: "date_created", - ActiveStatuses: []string{"active", "draft"}, - } - - // Validate duplicate entries with active status for today - err := validator.ValidateDuplicate(ctx, config, "dummy_id") - if err != nil { - return fmt.Errorf("validation failed: %w", err) - } - - // Validate once per day submission - err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit") - if err != nil { - return fmt.Errorf("daily submission limit exceeded: %w", err) - } - - return nil +func (h *` + data.Name + `Handler) validate` + data.Name + `Submission(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) error { + // Import the validation utility + validator := validation.NewDuplicateValidator(dbConn) + + // Use default configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + } + + // Validate duplicate entries with active status for today + err := validator.ValidateDuplicate(ctx, config, "dummy_id") + if err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + // Validate once per day submission + err = validator.ValidateOncePerDay(ctx, "` + data.TableName + `", "id", "date_created", "daily_limit") + if err != nil { + return fmt.Errorf("daily submission limit exceeded: %w", err) + } + + return nil } // Example usage of the validation utility with custom configuration -func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `.` + data.Name + `CreateRequest) error { - // Create validator instance - validator := validation.NewDuplicateValidator(dbConn) - - // Use custom configuration - config := validation.ValidationConfig{ - TableName: "` + data.TableName + `", - IDColumn: "id", - StatusColumn: "status", - DateColumn: "date_created", - ActiveStatuses: []string{"active", "draft"}, - AdditionalFields: map[string]interface{}{ - "name": req.Name, - }, - } - - // Validate with custom fields - fields := map[string]interface{}{ - "name": *req.Name, - } - - err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) - if err != nil { - return fmt.Errorf("custom validation failed: %w", err) - } - - return nil +func (h *` + data.Name + `Handler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `CreateRequest) error { + // Create validator instance + validator := validation.NewDuplicateValidator(dbConn) + + // Use custom configuration + config := validation.ValidationConfig{ + TableName: "` + data.TableName + `", + IDColumn: "id", + StatusColumn: "status", + DateColumn: "date_created", + ActiveStatuses: []string{"active", "draft"}, + AdditionalFields: map[string]interface{}{ + "name": req.Name, + }, + } + + // Validate with custom fields + fields := map[string]interface{}{ + "name": *req.Name, + } + + err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) + if err != nil { + return fmt.Errorf("custom validation failed: %w", err) + } + + return nil } // GetLastSubmissionTime example func (h *` + data.Name + `Handler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) { - validator := validation.NewDuplicateValidator(dbConn) - return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier) -}` - - return helperMethods + validator := validation.NewDuplicateValidator(dbConn) + return validator.GetLastSubmissionTime(ctx, "` + data.TableName + `", "id", "date_created", identifier) } +`) + } -// Keep existing functions for model generation and routes... -// (The remaining functions stay the same as in the original file) + // Helper methods untuk PUT operations + if data.HasPut { + helperMethods.WriteString(` + +// Database operations untuk UPDATE +func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, dbConn *sql.DB, req *` + data.Category + `Models.` + data.Name + `UpdateRequest) (*` + data.Category + `Models.` + data.Name + `, error) { + now := time.Now() + + query := "UPDATE ` + data.TableName + ` SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name" + row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) + + var item ` + data.Category + `Models.` + data.Name + ` + err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name) + if err != nil { + return nil, fmt.Errorf("failed to update ` + data.NameLower + `: %w", err) + } + + return &item, nil +} +`) + } + + // Helper methods untuk DELETE operations + if data.HasDelete { + helperMethods.WriteString(` + +// Database operations untuk DELETE +func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} +`) + } + + // Helper methods untuk DYNAMIC operations + if data.HasDynamic { + helperMethods.WriteString(` + // fetch` + data.Name + `sDynamic executes dynamic query + func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) { + // Setup query builders + countBuilder := utils.NewQueryBuilder("` + data.TableName + `"). + SetColumnMapping(map[string]string{ + // Add your column mappings here + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + // Add other allowed columns here + }) + + mainBuilder := utils.NewQueryBuilder("` + data.TableName + `"). + SetColumnMapping(map[string]string{ + // Add your column mappings here + }). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + // Add other allowed columns here + }) + + // Add default filter to exclude deleted records + if len(query.Filters) > 0 { + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }}, query.Filters...) + } else { + query.Filters = []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }} + } + + // Execute queries sequentially + var total int + var items []` + data.Category + `Models.` + data.Name + ` + + // 1. Get total count + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + + countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery) + if err != nil { + return nil, 0, fmt.Errorf("failed to build count query: %w", err) + } + + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // 2. Get main data + mainSQL, mainArgs, err := mainBuilder.BuildQuery(query) + if err != nil { + return nil, 0, fmt.Errorf("failed to build main query: %w", err) + } + + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + return nil, 0, fmt.Errorf("failed to execute main query: %w", err) + } + defer rows.Close() + + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + return nil, 0, fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) + } + items = append(items, item) + } + + if err := rows.Err(); err != nil { + return nil, 0, fmt.Errorf("rows iteration error: %w", err) + } + + return items, total, nil + } + `) + } + + return helperMethods.String() +} // ================= MODEL GENERATION ===================== func generateModelFile(data HandlerData, modelDir string) { + // Tentukan nama file model + modelFileName := data.NameLower + ".go" + modelFilePath := filepath.Join(modelDir, modelFileName) + + // Periksa apakah file model sudah ada + if _, err := os.Stat(modelFilePath); err == nil { + // File sudah ada, skip pembuatan model + fmt.Printf("Model %s already exists, skipping generation\n", data.Name) + return + } + // Tentukan import block var importBlock, nullablePrefix string @@ -1558,12 +2189,13 @@ type ` + data.Name + `Filter struct { DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` } ` - writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) + writeFile(modelFilePath, modelContent) + fmt.Printf("Successfully generated model: %s\n", modelFileName) } // ================= ROUTES GENERATION ===================== func updateRoutesFile(data HandlerData) { - routesFile := "internal/routes/v1/routes.go" + routesFile := "../../internal/routes/v1/routes.go" content, err := os.ReadFile(routesFile) if err != nil { fmt.Printf("⚠️ Could not read routes.go: %v\n", err) @@ -1574,6 +2206,9 @@ func updateRoutesFile(data HandlerData) { routesContent := string(content) + // Clean up duplicate routes first + routesContent = cleanupDuplicateRoutes(routesContent, data) + // Build import path var importPath, importAlias string if data.Category != "models" { @@ -1594,129 +2229,230 @@ func updateRoutesFile(data HandlerData) { } } + // Check if routes for this specific endpoint already exist + if routeBlockExists(routesContent, data) { + if *verboseFlag { + fmt.Printf("✅ Routes for %s (%s) already exist, skipping...\n", data.Name, getGroupPath(data)) + } + return + } + // Build new routes in protected group format newRoutes := generateProtectedRouteBlock(data) - // Insert above protected routes marker - insertMarker := "// ============= PUBLISHED ROUTES ===============================================" + // Use the correct marker for insertion + insertMarker := ` // ============================================================================= + // PUBLISHED ROUTES + // ============================================================================= +` + + // Find the position to insert routes if strings.Contains(routesContent, insertMarker) { - if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { - // Insert before the marker - routesContent = strings.Replace(routesContent, insertMarker, - newRoutes+"\n\t"+insertMarker, 1) - } else { - fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name) - return + // Insert after the marker block (including the newlines) + markerIndex := strings.Index(routesContent, insertMarker) + if markerIndex != -1 { + // Find the end of the marker block (including the newlines) + endOfMarker := markerIndex + len(insertMarker) + + // Insert the new routes after the marker + routesContent = routesContent[:endOfMarker] + "\n" + newRoutes + routesContent[endOfMarker:] + + if *verboseFlag { + fmt.Printf("📍 Inserted routes after PUBLISHED ROUTES marker\n") + } } } else { - // Fallback: insert at end of setupV1Routes function - setupFuncEnd := "\treturn r" - if strings.Contains(routesContent, setupFuncEnd) { - routesContent = strings.Replace(routesContent, setupFuncEnd, - newRoutes+"\n\n\t"+setupFuncEnd, 1) + // Fallback: try to find alternative markers + alternativeMarkers := []string{ + "// ============= PUBLISHED ROUTES ===============================================", + "// PUBLISHED ROUTES", + "return r", // End of function + } + + inserted := false + for _, marker := range alternativeMarkers { + if strings.Contains(routesContent, marker) { + if marker == "return r" { + // Insert before the return statement + routesContent = strings.Replace(routesContent, "\t"+marker, + newRoutes+"\n\n\t"+marker, 1) + } else { + // Insert after the marker + routesContent = strings.Replace(routesContent, marker, + marker+"\n"+newRoutes, 1) + } + if *verboseFlag { + fmt.Printf("📍 Inserted routes using alternative marker: %s\n", marker) + } + inserted = true + break + } + } + + if !inserted { + // Last resort: append at the end of the file + routesContent += "\n" + newRoutes + if *verboseFlag { + fmt.Printf("📍 Appended routes at the end of file\n") + } } } if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { - fmt.Printf("Error writing routes.go: %v\n", err) + logError("Error writing routes.go", err, *verboseFlag) return } - fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name) + if *verboseFlag { + fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name) + } +} + +// routeBlockExists checks if a route block for the specific endpoint already exists +func routeBlockExists(content string, data HandlerData) bool { + groupPath := getGroupPath(data) + handlerName := getHandlerName(data) + + // Build the expected route block pattern - lebih spesifik + patterns := []string{ + // Pattern 1: Full route block dengan comment (paling spesifik) + fmt.Sprintf("// %s endpoints\n %sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", + data.Name, handlerName, handlerName, data.Name, handlerName, groupPath), + + // Pattern 2: Handler dan Group declaration (tanpa comment) + fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()\n %sGroup := v1.Group(\"/%s\")", + handlerName, handlerName, data.Name, handlerName, groupPath), + + // Pattern 3: Group declaration saja (jika handler sudah ada di import) + fmt.Sprintf("%sGroup := v1.Group(\"/%s\")", handlerName, groupPath), + + // Pattern 4: Cari kombinasi handler dan group dengan spasi yang berbeda + fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name), + } + + // Check if any of the patterns exist + for _, pattern := range patterns { + if strings.Contains(content, pattern) { + if *verboseFlag { + fmt.Printf("🔍 Found existing route pattern: %s\n", pattern) + } + return true + } + } + + // Additional check: look for any route with the same group path + groupPattern := fmt.Sprintf("v1.Group(\"/%s\")", groupPath) + if strings.Contains(content, groupPattern) { + if *verboseFlag { + fmt.Printf("🔍 Found existing group pattern: %s\n", groupPattern) + } + return true + } + + return false +} + +// getGroupPath returns the group path for the given data +func getGroupPath(data HandlerData) string { + if data.Category != "models" { + return strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) + } + return strings.ToLower(data.Name) +} + +// getHandlerName returns the handler name for the given data +func getHandlerName(data HandlerData) string { + if data.Category != "models" { + return data.Category + data.Name + } + return strings.ToLower(data.Name) } func generateProtectedRouteBlock(data HandlerData) string { - // fmt.Printf("📁 Group Part: %s\n", groupPath) var sb strings.Builder - var importPath, groupPath string - if data.Category != "models" { - importPath = data.Category + data.Name - groupPath = strings.ToLower(data.Category) + "/" + data.NameLower - } else { - importPath = data.NameLower - groupPath = data.NameLower - } + handlerName := getHandlerName(data) + groupPath := getGroupPath(data) + // Komentar dan deklarasi handler & grup sb.WriteString("// ") sb.WriteString(data.Name) sb.WriteString(" endpoints\n") sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler := ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handlers.New") sb.WriteString(data.Name) sb.WriteString("Handler()\n ") - sb.WriteString(importPath) - + sb.WriteString(handlerName) sb.WriteString("Group := v1.Group(\"/") sb.WriteString(groupPath) sb.WriteString("\")\n {\n ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString(")\n") if data.HasDynamic { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/dynamic\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Dynamic) // Route baru\n") } if data.HasSearch { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/search\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Search") sb.WriteString(data.Name) sb.WriteString("Advanced) // Route pencarian\n") } sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("ByID)\n") if data.HasPost { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.POST(\"\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Create") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasPut { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.PUT(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Update") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasDelete { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.DELETE(\"/:id\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Delete") sb.WriteString(data.Name) sb.WriteString(")\n") } if data.HasStats { sb.WriteString(" ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Group.GET(\"/stats\", ") - sb.WriteString(importPath) + sb.WriteString(handlerName) sb.WriteString("Handler.Get") sb.WriteString(data.Name) sb.WriteString("Stats)\n") @@ -1725,6 +2461,136 @@ func generateProtectedRouteBlock(data HandlerData) string { return sb.String() } +func cleanupDuplicateRoutes(content string, data HandlerData) string { + // Implement getGroupPath logic directly + var groupPath string + if data.Category != "models" { + groupPath = strings.ToLower(data.Category) + "/" + strings.ToLower(data.Name) + } else { + groupPath = strings.ToLower(data.Name) + } + + // Implement getHandlerName logic directly + var handlerName string + if data.Category != "models" { + handlerName = data.Category + data.Name + } else { + handlerName = strings.ToLower(data.Name) + } + + // Split content by lines for better processing + lines := strings.Split(content, "\n") + + var cleanedLines []string + inRouteBlock := false + routeBlockFound := false + blockStartLine := -1 + + for i, line := range lines { + // Check if this line starts a route block for our endpoint + // Look for comment pattern first + if strings.Contains(line, fmt.Sprintf("// %s endpoints", data.Name)) { + if routeBlockFound { + // This is a duplicate, skip it + if *verboseFlag { + fmt.Printf("🧹 Skipping duplicate route block for %s at line %d\n", data.Name, i+1) + } + inRouteBlock = true + blockStartLine = i + continue + } else { + // This is the first occurrence, keep it + routeBlockFound = true + inRouteBlock = true + blockStartLine = i + cleanedLines = append(cleanedLines, line) + continue + } + } + + // Alternative pattern: look for handler declaration without comment + if !inRouteBlock && strings.Contains(line, fmt.Sprintf("%sHandler := %sHandlers.New%sHandler()", handlerName, handlerName, data.Name)) { + if routeBlockFound { + // This is a duplicate, skip it + if *verboseFlag { + fmt.Printf("🧹 Skipping duplicate handler declaration for %s at line %d\n", data.Name, i+1) + } + inRouteBlock = true + blockStartLine = i + continue + } else { + // This is the first occurrence, keep it + routeBlockFound = true + inRouteBlock = true + blockStartLine = i + cleanedLines = append(cleanedLines, line) + continue + } + } + + // Third pattern: look for group declaration (for cases without handler declaration on same line) + if !inRouteBlock && strings.Contains(line, fmt.Sprintf("Group := v1.Group(\"/%s\")", groupPath)) { + if routeBlockFound { + // This is a duplicate, skip it + if *verboseFlag { + fmt.Printf("🧹 Skipping duplicate group declaration for %s at line %d\n", data.Name, i+1) + } + inRouteBlock = true + blockStartLine = i + continue + } else { + // This is the first occurrence, keep it + routeBlockFound = true + inRouteBlock = true + blockStartLine = i + cleanedLines = append(cleanedLines, line) + continue + } + } + + // If we're in a route block, check if we've reached the end + if inRouteBlock { + // Count braces to determine if we're at the end of the block + openBraces := strings.Count(line, "{") + closeBraces := strings.Count(line, "}") + + // If we have more closing braces than opening braces, we might be at the end + if closeBraces > openBraces { + // Check if this line contains the closing brace for the route group + if strings.Contains(line, "}") && (strings.Contains(line, "Group") || strings.Contains(line, "handler")) { + // This is likely the end of our route block + if routeBlockFound && blockStartLine != -1 { + cleanedLines = append(cleanedLines, line) + inRouteBlock = false + blockStartLine = -1 + continue + } + } + } + + // Additional check: look for standalone closing brace that might end the block + if strings.TrimSpace(line) == "}" && inRouteBlock { + // This might be the end of our route block + if routeBlockFound && blockStartLine != -1 { + cleanedLines = append(cleanedLines, line) + inRouteBlock = false + blockStartLine = -1 + continue + } + } + + // Add the line if we're keeping this block + if routeBlockFound { + cleanedLines = append(cleanedLines, line) + } + } else { + // Not in a route block, just add the line + cleanedLines = append(cleanedLines, line) + } + } + + return strings.Join(cleanedLines, "\n") +} func printRoutesSample(data HandlerData) { fmt.Print(generateProtectedRouteBlock(data)) fmt.Println() @@ -1733,8 +2599,28 @@ func printRoutesSample(data HandlerData) { // ================= UTILITY FUNCTIONS ===================== func writeFile(filename, content string) { if err := os.WriteFile(filename, []byte(content), 0644); err != nil { - fmt.Printf("❌ Error creating file %s: %v\n", filename, err) + logError(fmt.Sprintf("Error creating file %s", filename), err, *verboseFlag) return } - fmt.Printf("✅ Generated: %s\n", filename) + if *verboseFlag { + fmt.Printf("✅ Generated: %s\n", filename) + } +} + +// Enhanced error logging function +func logError(message string, err error, verbose bool) { + if verbose { + log.Printf("❌ ERROR: %s - %v", message, err) + } else { + log.Printf("❌ ERROR: %s", message) + } +} + +// Success logging function +func logSuccess(message string, details ...string) { + fmt.Printf("✅ %s", message) + for _, detail := range details { + fmt.Printf(" - %s", detail) + } + fmt.Println() } diff --git a/tools/general/old/generate-handlerold.go b/tools/general/old/generate-handlerold.go new file mode 100644 index 0000000..1038efc --- /dev/null +++ b/tools/general/old/generate-handlerold.go @@ -0,0 +1,2000 @@ +package main + +import ( + "fmt" + "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 +} + +// parseEntityPath - Logic parsing yang diperbaiki +func parseEntityPath(entityPath string) (*PathInfo, error) { + if strings.TrimSpace(entityPath) == "" { + return nil, fmt.Errorf("entity path cannot be empty") + } + var pathInfo PathInfo + parts := strings.Split(entityPath, "/") + // 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 + for i, part := range parts { + if strings.TrimSpace(part) == "" { + return nil, fmt.Errorf("empty path segment at position %d", 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 +} + +// 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 - Buat direktori sesuai struktur path +func createDirectories(pathInfo *PathInfo) (string, string, error) { + var handlerDir, modelDir string + + if pathInfo.DirPath != "" { + handlerDir = filepath.Join("internal", "handlers", pathInfo.DirPath) + modelDir = filepath.Join("internal", "models", pathInfo.DirPath) + } else { + handlerDir = filepath.Join("internal", "handlers") + modelDir = filepath.Join("internal", "models") + } + + // Create directories + 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) + } + } + + 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 + } +} + +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 +} + +func generateFromServicesConfig(config *ServicesConfig) { + for serviceName, service := range config.Services { + fmt.Printf("🔧 Processing service: %s\n", serviceName) + + for endpointName, endpoint := range service.Endpoints { + fmt.Printf(" 📍 Processing endpoint: %s\n", endpointName) + + for functionName, function := range endpoint.Functions { + fmt.Printf(" ⚙️ Processing function: %s\n", functionName) + + // Parse entity path from endpoint name + pathInfo, err := parseEntityPath(endpointName) + if err != nil { + fmt.Printf("❌ Error parsing entity path '%s': %v\n", endpointName, err) + continue + } + + // Override category from service config + if service.Category != "" { + pathInfo.Category = service.Category + } + + // Set directory path from handler_folder if specified + if endpoint.HandlerFolder != "" { + pathInfo.DirPath = endpoint.HandlerFolder + } + + // Use methods from function config + methods := function.Methods + if len(methods) == 0 { + methods = []string{"get", "post", "put", "delete", "dynamic", "search"} + } + + // Validate methods + if err := validateMethods(methods); err != nil { + fmt.Printf("❌ Invalid methods for function '%s': %v\n", functionName, err) + continue + } + + // Override table name if specified + tableName := endpoint.TableName + if tableName == "" { + tableName = generateTableName(pathInfo) + } + + // Generate handler data with 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, + HasPagination: function.HasPagination, + HasFilter: function.HasFilter, + HasSearch: function.HasSearch, + HasStats: function.HasStats, + HasDynamic: function.HasDynamic, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Set methods from function config + setMethods(&data, methods) + + // Create directories + handlerDir, modelDir, err := createDirectories(pathInfo) + if err != nil { + fmt.Printf("❌ Error creating directories: %v\n", err) + continue + } + + // Generate files + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) + updateRoutesFile(data) + + // Success output + fmt.Printf(" ✅ Successfully generated handler: %s\n", entityName) + if pathInfo.Category != "" { + fmt.Printf(" 📁 Category: %s\n", pathInfo.Category) + } + if pathInfo.DirPath != "" { + fmt.Printf(" 📂 Path: %s\n", pathInfo.DirPath) + } + fmt.Printf(" 📄 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) + fmt.Printf(" 📄 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) + fmt.Printf(" 🗄️ Table: %s\n", tableName) + fmt.Printf(" 🛠️ Methods: %s\n", strings.Join(methods, ", ")) + fmt.Println() + } + } + } +} + +func main() { + // Check for services-config.yaml first (new format) + servicesConfigPath := "services-config.yaml" + servicesConfig, err := loadServicesConfig(servicesConfigPath) + if err == nil { + // Use services config + fmt.Printf("📄 Using services configuration from %s\n", servicesConfigPath) + generateFromServicesConfig(servicesConfig) + return + } + + // Fallback to old config-handler.yml + configPath := "config-handler.yml" + config, err := loadConfig(configPath) + if err == nil { + // Generate from old config + fmt.Printf("📄 Using legacy configuration from %s\n", configPath) + for _, entity := range config.Entities { + pathInfo, err := parseEntityPath(entity.Name) + if err != nil { + fmt.Printf("❌ Error parsing entity path '%s': %v\n", entity.Name, err) + 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 { + fmt.Printf("❌ Invalid methods for entity '%s': %v\n", entity.Name, err) + continue + } + + generateForEntity(pathInfo, methods) + } + return + } + + // No config files found, fallback to command line arguments + fmt.Printf("⚠️ No config files found (services-config.yaml or config-handler.yml), falling back to command line arguments\n") + + 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.") + os.Exit(1) + } + + // Parse entity path + entityPath := strings.TrimSpace(os.Args[1]) + pathInfo, err := parseEntityPath(entityPath) + if err != nil { + fmt.Printf("❌ Error parsing path: %v\n", err) + 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 { + fmt.Printf("❌ %v\n", err) + 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 { + fmt.Printf("❌ Error creating directories: %v\n", err) + return + } + + // Generate files + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) + updateRoutesFile(data) + + // Success output + fmt.Printf("✅ Successfully generated handler: %s\n", entityName) + if pathInfo.Category != "" { + fmt.Printf("📁 Category: %s\n", pathInfo.Category) + } + if pathInfo.DirPath != "" { + fmt.Printf("📂 Path: %s\n", pathInfo.DirPath) + } + fmt.Printf("📄 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) + fmt.Printf("📄 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) + fmt.Printf("🗄️ Table: %s\n", tableName) + fmt.Printf("🛠️ Methods: %s\n", strings.Join(methods, ", ")) + fmt.Println() +} + +// ================= HANDLER GENERATION ===================== +func generateHandlerFile(data HandlerData, handlerDir string) { + // var modelsImportPath string + // if data.Category != "" { + // modelsImportPath = data.ModuleName + "/internal/models/" + data.Category + // } else { + // modelsImportPath = data.ModuleName + "/internal/models" + // } + + // pakai strings.Builder biar lebih clean + var handlerContent strings.Builder + + // Header + 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 + if data.HasDynamic || data.HasSearch { + handlerContent.WriteString(` utils "` + data.ModuleName + `/internal/utils/filters"` + "\n") + } + + 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") + handlerContent.WriteString(" " + data.NameLower + "validate.RegisterValidation(\"" + data.NameLower + "_status\", validate" + data.Name + "Status)\n") + handlerContent.WriteString(" if " + data.NameLower + "db == nil {\n") + handlerContent.WriteString(" log.Fatal(\"Failed to initialize database connection\")\n") + handlerContent.WriteString(" }\n") + handlerContent.WriteString(" })\n") + handlerContent.WriteString("}\n\n") + + // Custom validation + handlerContent.WriteString("// Custom validation for " + data.NameLower + " status\n") + handlerContent.WriteString("func validate" + data.Name + "Status(fl validator.FieldLevel) bool {\n") + handlerContent.WriteString(" return models.IsValidStatus(fl.Field().String())\n") + handlerContent.WriteString("}\n\n") + + // 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 + 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 + 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 ` + +// Search` + data.Name + `Advanced provides advanced search capabilities +func (h *` + data.Name + `Handler) Search` + data.Name + `Advanced(c *gin.Context) { + // Parse complex search parameters + searchQuery := c.Query("q") + if searchQuery == "" { + h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest) + return + } + + // Build dynamic query for search + query := utils.DynamicQuery{ + Fields: []string{"*"}, + Filters: []utils.FilterGroup{{ + Filters: []utils.DynamicFilter{ + { + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }, + { + Column: "name", + Operator: utils.OpContains, + Value: searchQuery, + LogicOp: "OR", + }, + }, + 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 + items, 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: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +}` +} + +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 { + helperMethods := ` + +// Database operations +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 +} + +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 +} + +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 +} + +func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete ` + data.NameLower + `: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter ` + data.Category + `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) + } + + log.Printf("Successfully fetched %d ` + data.NamePlural + ` with filters applied", len(items)) + return items, nil +}` + + // Add dynamic fetch method if needed + if data.HasDynamic { + helperMethods += ` + +// fetchRetribusisDynamic executes dynamic query +func (h *` + data.Name + `Handler) fetch` + data.Name + `sDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]` + data.Category + `Models.` + data.Name + `, int, error) { + // Setup query builder + builder := utils.NewQueryBuilder("` + data.TableName + `"). + SetAllowedColumns([]string{ + "id", "status", "sort", "user_created", "date_created", + "user_updated", "date_updated", "name", + }) + + // Add default filter to exclude deleted records + query.Filters = append([]utils.FilterGroup{{ + Filters: []utils.DynamicFilter{{ + Column: "status", + Operator: utils.OpNotEqual, + Value: "deleted", + }}, + LogicOp: "AND", + }}, query.Filters...) + + // Execute concurrent queries + var ( + items [] ` + data.Category + `Models.` + data.Name + ` + total int + wg sync.WaitGroup + errChan = make(chan error, 2) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + countQuery := query + countQuery.Limit = 0 + countQuery.Offset = 0 + countSQL, countArgs, err := builder.BuildCountQuery(countQuery) + if err != nil { + errChan <- fmt.Errorf("failed to build count query: %w", err) + return + } + if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil { + errChan <- fmt.Errorf("failed to get total count: %w", err) + return + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + mainSQL, mainArgs, err := builder.BuildQuery(query) + if err != nil { + errChan <- fmt.Errorf("failed to build main query: %w", err) + return + } + + rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...) + if err != nil { + errChan <- fmt.Errorf("failed to execute main query: %w", err) + return + } + defer rows.Close() + + var results []` + data.Category + `Models.` + data.Name + ` + for rows.Next() { + item, err := h.scan` + data.Name + `(rows) + if err != nil { + errChan <- fmt.Errorf("failed to scan ` + data.NameLower + `: %w", err) + return + } + results = append(results, item) + } + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("rows iteration error: %w", err) + return + } + + mu.Lock() + items = results + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, 0, err + } + } + + return items, total, nil +} +` + } + + helperMethods += ` +// Optimized scanning function +func (h *` + data.Name + `Handler) scan` + data.Name + `(rows *sql.Rows) (` + data.Category + `Models.` + data.Name + `, error) { + var item ` + data.Category + `Models.` + data.Name + ` + + // Scan into individual fields to handle nullable types properly + err := rows.Scan( + &item.ID, + &item.Status, + &item.Sort.Int32, &item.Sort.Valid, // models.NullableInt32 + &item.UserCreated.String, &item.UserCreated.Valid, // sql.NullString + &item.DateCreated.Time, &item.DateCreated.Valid, // sql.NullTime + &item.UserUpdated.String, &item.UserUpdated.Valid, // sql.NullString + &item.DateUpdated.Time, &item.DateUpdated.Valid, // sql.NullTime + &item.Name.String, &item.Name.Valid, // sql.NullString + ) + + return item, err +} + +func (h *` + data.Name + `Handler) 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 +} + +// 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 +} + +// Enhanced error handling +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(), + }) +} + +// Parse pagination parameters dengan validation yang lebih ketat +func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + return limit, offset, nil +} + +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 +} + +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, + } +} + +// 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) +}` + + return helperMethods +} + +// Keep existing functions for model generation and routes... +// (The remaining functions stay the same as in the original file) + +// ================= MODEL GENERATION ===================== +func generateModelFile(data HandlerData, modelDir string) { + // 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(filepath.Join(modelDir, data.NameLower+".go"), modelContent) +} + +// ================= 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) + + // 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) + } + } + + // Build new routes in protected group format + newRoutes := generateProtectedRouteBlock(data) + + // Insert above protected routes marker + insertMarker := "// ============= PUBLISHED ROUTES ===============================================" + if strings.Contains(routesContent, insertMarker) { + if !strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { + // Insert before the marker + routesContent = strings.Replace(routesContent, insertMarker, + newRoutes+"\n\t"+insertMarker, 1) + } else { + fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name) + return + } + } else { + // Fallback: insert at end of setupV1Routes function + setupFuncEnd := "\treturn r" + if strings.Contains(routesContent, setupFuncEnd) { + routesContent = strings.Replace(routesContent, setupFuncEnd, + newRoutes+"\n\n\t"+setupFuncEnd, 1) + } + } + + if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { + fmt.Printf("Error writing routes.go: %v\n", err) + return + } + + fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name) +} + +func generateProtectedRouteBlock(data HandlerData) string { + // fmt.Printf("📁 Group Part: %s\n", groupPath) + var sb strings.Builder + var importPath, groupPath string + if data.Category != "models" { + importPath = data.Category + data.Name + groupPath = strings.ToLower(data.Category) + "/" + data.NameLower + } else { + importPath = data.NameLower + groupPath = data.NameLower + } + // Komentar dan deklarasi handler & grup + sb.WriteString("// ") + sb.WriteString(data.Name) + sb.WriteString(" endpoints\n") + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Handler := ") + sb.WriteString(importPath) + sb.WriteString("Handlers.New") + sb.WriteString(data.Name) + sb.WriteString("Handler()\n ") + sb.WriteString(importPath) + + sb.WriteString("Group := v1.Group(\"/") + sb.WriteString(groupPath) + sb.WriteString("\")\n {\n ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString(")\n") + + if data.HasDynamic { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/dynamic\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("Dynamic) // Route baru\n") + } + if data.HasSearch { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/search\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Search") + sb.WriteString(data.Name) + sb.WriteString("Advanced) // Route pencarian\n") + } + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("ByID)\n") + + if data.HasPost { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.POST(\"\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Create") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasPut { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.PUT(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Update") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasDelete { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.DELETE(\"/:id\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Delete") + sb.WriteString(data.Name) + sb.WriteString(")\n") + } + if data.HasStats { + sb.WriteString(" ") + sb.WriteString(importPath) + sb.WriteString("Group.GET(\"/stats\", ") + sb.WriteString(importPath) + sb.WriteString("Handler.Get") + sb.WriteString(data.Name) + sb.WriteString("Stats)\n") + } + sb.WriteString(" }\n") + return sb.String() +} + +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 { + fmt.Printf("❌ Error creating file %s: %v\n", filename, err) + return + } + fmt.Printf("✅ Generated: %s\n", filename) +} diff --git a/tools/general/services-config.yaml b/tools/general/services-config.yaml new file mode 100644 index 0000000..40944b3 --- /dev/null +++ b/tools/general/services-config.yaml @@ -0,0 +1,250 @@ +global: + module_name: "api-service" + output_dir: "internal/handlers" + enable_swagger: true + enable_logging: true + +services: + retribusi: + name: "Retribusi" + category: "retribusi" + package: "retribusi" + description: "Retribusi service for tariff and billing management" + base_url: "" + timeout: 30 + retry_count: 3 + + endpoints: + # retribusi: + # description: "Retribusi tariff management" + # handler_folder: "retribusi" + # handler_file: "retribusi.go" + # handler_name: "Retribusi" + # table_name: "data_retribusi" + # functions: + # list: + # methods: ["GET"] + # path: "/" + # get_routes: "/" + # get_path: "/" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Get retribusi list with pagination and filters" + # summary: "Get Retribusi List" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_pagination: true + # has_filter: true + # has_search: true + # has_stats: true + + # get: + # methods: ["GET"] + # path: "/:id" + # get_routes: "/:id" + # get_path: "/:id" + # model: "Retribusi" + # response_model: "RetribusiGetByIDResponse" + # description: "Get retribusi by ID" + # summary: "Get Retribusi by ID" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + + # dynamic: + # methods: ["GET"] + # path: "/dynamic" + # get_routes: "/dynamic" + # get_path: "/dynamic" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Get retribusi with dynamic filtering" + # summary: "Get Retribusi Dynamic" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_dynamic: true + + # search: + # methods: ["GET"] + # path: "/search" + # get_routes: "/search" + # get_path: "/search" + # model: "Retribusi" + # response_model: "RetribusiGetResponse" + # description: "Search retribusi" + # summary: "Search Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 300 + # has_search: true + + # create: + # methods: ["POST"] + # path: "/" + # post_routes: "/" + # post_path: "/" + # model: "RetribusiCreateRequest" + # response_model: "RetribusiCreateResponse" + # request_model: "RetribusiCreateRequest" + # description: "Create new retribusi" + # summary: "Create Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # update: + # methods: ["PUT"] + # path: "/:id" + # put_routes: "/:id" + # put_path: "/:id" + # model: "RetribusiUpdateRequest" + # response_model: "RetribusiUpdateResponse" + # request_model: "RetribusiUpdateRequest" + # description: "Update retribusi" + # summary: "Update Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # delete: + # methods: ["DELETE"] + # path: "/:id" + # delete_routes: "/:id" + # delete_path: "/:id" + # model: "Retribusi" + # response_model: "RetribusiDeleteResponse" + # description: "Delete retribusi" + # summary: "Delete Retribusi" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: false + # enable_database: true + # cache_ttl: 0 + + # stats: + # methods: ["GET"] + # path: "/stats" + # get_routes: "/stats" + # get_path: "/stats" + # model: "AggregateData" + # response_model: "AggregateData" + # description: "Get retribusi statistics" + # summary: "Get Retribusi Stats" + # tags: ["Retribusi"] + # require_auth: true + # cache_enabled: true + # enable_database: true + # cache_ttl: 180 + # has_stats: true + + # Example of another service + user: + name: "User" + category: "user" + package: "user" + description: "User management service" + base_url: "" + timeout: 30 + retry_count: 3 + + endpoints: + user: + description: "User management endpoints" + handler_folder: "retribusi" + handler_file: "user.go" + handler_name: "User" + table_name: "data_user" + functions: + list: + methods: ["GET"] + path: "/" + get_routes: "/" + get_path: "/" + model: "User" + response_model: "UserGetResponse" + description: "Get user list with pagination" + summary: "Get User List" + tags: ["User"] + require_auth: true + cache_enabled: true + enable_database: true + cache_ttl: 300 + has_pagination: true + has_filter: true + has_search: true + + get: + methods: ["GET"] + path: "/:id" + get_routes: "/:id" + get_path: "/:id" + model: "User" + response_model: "UserGetByIDResponse" + description: "Get user by ID" + summary: "Get User by ID" + tags: ["User"] + require_auth: true + cache_enabled: true + enable_database: true + cache_ttl: 300 + + create: + methods: ["POST"] + path: "/" + post_routes: "/" + post_path: "/" + model: "UserCreateRequest" + response_model: "UserCreateResponse" + request_model: "UserCreateRequest" + description: "Create new user" + summary: "Create User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0 + + update: + methods: ["PUT"] + path: "/:id" + put_routes: "/:id" + put_path: "/:id" + model: "UserUpdateRequest" + response_model: "UserUpdateResponse" + request_model: "UserUpdateRequest" + description: "Update user" + summary: "Update User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0 + + delete: + methods: ["DELETE"] + path: "/:id" + delete_routes: "/:id" + delete_path: "/:id" + model: "User" + response_model: "UserDeleteResponse" + description: "Delete user" + summary: "Delete User" + tags: ["User"] + require_auth: true + cache_enabled: false + enable_database: true + cache_ttl: 0