Perbaikan generate

This commit is contained in:
2025-10-20 09:46:08 +07:00
parent 21cc6aacdb
commit f253e65736
2 changed files with 617 additions and 266 deletions
+312 -138
View File
@@ -6,6 +6,7 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -21,6 +22,7 @@ type HandlerData struct {
DirPath string // Path direktori lengkap
ModuleName string
TableName string
TableSchema []ColumnConfig // Untuk penyimpanan schema
HasGet bool
HasPost bool
HasPut bool
@@ -71,6 +73,20 @@ type ServiceConfig struct {
Endpoints map[string]EndpointConfig `yaml:"endpoints"`
}
// SchemaConfig represents a schema configuration
type SchemaConfig struct {
Columns []ColumnConfig `yaml:"columns"`
}
// ColumnConfig represents a column configuration
type ColumnConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Nullable bool `yaml:"nullable,omitempty"`
GoType string `yaml:"go_type,omitempty"` // Untuk override tipe Go secara manual
PrimaryKey bool `yaml:"primary_key,omitempty"`
}
// EndpointConfig represents an endpoint configuration
type EndpointConfig struct {
Description string `yaml:"description"`
@@ -78,6 +94,7 @@ type EndpointConfig struct {
HandlerFile string `yaml:"handler_file"`
HandlerName string `yaml:"handler_name"`
TableName string `yaml:"table_name,omitempty"`
Schema SchemaConfig `yaml:"schema,omitempty"`
Functions map[string]FunctionConfig `yaml:"functions"`
}
@@ -396,14 +413,15 @@ func generateFromServicesConfig(config *ServicesConfig) {
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"),
Name: entityName,
NameLower: entityLower,
NamePlural: entityPlural,
Category: pathInfo.Category,
DirPath: pathInfo.DirPath,
ModuleName: config.Global.ModuleName,
TableName: tableName,
TableSchema: endpoint.Schema.Columns,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
// Set methods berdasarkan aggregated methods
@@ -444,14 +462,19 @@ func generateFromServicesConfig(config *ServicesConfig) {
continue
}
if shouldSkipExistingFile(modelPath, "model") {
fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath)
continue
if len(data.TableSchema) > 0 {
if shouldSkipExistingFile(modelPath, "model") {
fmt.Printf("⚠️ Skipping model generation: %s\n", modelPath)
} else {
generateModelFile(data, modelDir) // Memanggil fungsi baru
}
} else {
fmt.Printf("⚠️ Skipping model generation for '%s' because no schema is defined in the config.\n", entityName)
}
// Generate files (SEKALI SAJA per endpoint)
generateHandlerFile(data, handlerDir)
generateModelFile(data, modelDir)
// generateModelFile(data, modelDir)
// HANYA UPDATE ROUTES SEKALI PER ENDPOINT setelah semua fungsi di-aggregate
updateRoutesFile(data)
@@ -483,7 +506,8 @@ func main() {
}
// Check for services-config.yaml first (new format)
servicesConfig, err := loadServicesConfig(configPath)
servicesConfig, err :=
loadServicesConfig(configPath)
if err == nil {
// Use services config
if *verboseFlag {
@@ -2021,20 +2045,10 @@ func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context,
// ================= MODEL GENERATION =====================
func generateModelFile(data HandlerData, modelDir string) {
// Tentukan nama file model
modelFileName := data.NameLower + ".go"
modelFilePath := filepath.Join(modelDir, modelFileName)
// Periksa apakah file model sudah ada
if _, err := os.Stat(modelFilePath); err == nil {
// File sudah ada, skip pembuatan model
fmt.Printf("Model %s already exists, skipping generation\n", data.Name)
return
}
// Tentukan import block
var importBlock, nullablePrefix string
if data.Category == "models" {
importBlock = `import (
"database/sql"
@@ -2053,144 +2067,304 @@ func generateModelFile(data HandlerData, modelDir string) {
`
}
modelContent := `package ` + data.Category + `
var modelContent strings.Builder
modelContent.WriteString(fmt.Sprintf("package %s\n\n", data.Category))
modelContent.WriteString(importBlock)
` + importBlock + `
// Generate main struct
modelContent.WriteString(fmt.Sprintf("// %s represents the data structure for the %s table\n", data.Name, data.TableName))
modelContent.WriteString("// with proper null handling and optimized JSON marshaling\n")
modelContent.WriteString(fmt.Sprintf("type %s struct {\n", data.Name))
// ` + 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\"`" + `
}
for _, col := range data.TableSchema {
fieldName := snakeToPascal(col.Name)
goType, _, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
jsonTag := snakeToCamel(col.Name)
dbTag := col.Name // Gunakan nama kolom langsung
jsonTagValue := jsonTag
if col.Nullable {
jsonTagValue += ",omitempty"
}
modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" db:\"%s\"`\n", fieldName, goType, jsonTagValue, dbTag))
}
modelContent.WriteString("}\n\n")
// 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),
}
// Generate MarshalJSON method
modelContent.WriteString(fmt.Sprintf("// Custom JSON marshaling for %s so NULL values don't appear in response\n", data.Name))
modelContent.WriteString(fmt.Sprintf("func (r %s) MarshalJSON() ([]byte, error) {\n", data.Name))
modelContent.WriteString(fmt.Sprintf(" type Alias %s\n", data.Name))
modelContent.WriteString(" aux := &struct {\n *Alias\n")
for _, col := range data.TableSchema {
if !col.Nullable {
continue
}
fieldName := snakeToPascal(col.Name)
_, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
auxType := "*" + baseType
jsonTag := snakeToCamel(col.Name) + ",omitempty"
modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", fieldName, auxType, jsonTag))
}
modelContent.WriteString(" }{\n Alias: (*Alias)(&r),\n }\n\n")
for _, col := range data.TableSchema {
if !col.Nullable {
continue
}
fieldName := snakeToPascal(col.Name)
_, _, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n aux.%s = &r.%s.%s\n }\n", fieldName, fieldName, fieldName, valueType))
}
modelContent.WriteString(" return json.Marshal(aux)\n}\n\n")
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)
}
// Generate helper methods
for _, col := range data.TableSchema {
if !col.Nullable {
continue
}
fieldName := snakeToPascal(col.Name)
_, baseType, valueType := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
var zeroValue string
switch baseType {
case "string":
zeroValue = `""`
case "int32", "int64", "float64":
zeroValue = "0"
case "bool":
zeroValue = "false"
case "time.Time":
zeroValue = "time.Time{}"
default:
zeroValue = "nil"
}
modelContent.WriteString(fmt.Sprintf("// Helper method to safely get %s\n", fieldName))
modelContent.WriteString(fmt.Sprintf("func (r *%s) Get%s() %s {\n", data.Name, fieldName, baseType))
modelContent.WriteString(fmt.Sprintf(" if r.%s.Valid {\n return r.%s.%s\n }\n return %s\n}\n\n", fieldName, fieldName, valueType, zeroValue))
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *` + data.Name + `) GetName() string {
if r.Name.Valid {
return r.Name.String
}
return ""
}
`
// Generate request/response structs
excludedFields := map[string]bool{"id": true, "date_created": true, "date_updated": true, "user_created": true, "user_updated": true}
var createFields, updateFields []ColumnConfig
for _, col := range data.TableSchema {
if excludedFields[strings.ToLower(col.Name)] {
continue
}
createFields = append(createFields, col)
updateCol := col
updateFields = append(updateFields, updateCol)
}
// 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\"`" + `
modelContent.WriteString(fmt.Sprintf(`// Response struct for GET by ID
type %sGetByIDResponse struct {
Message string `+"`json:\"message\"`"+`
Data *%s `+"`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\"`" + `
// Enhanced GET response with pagination and aggregation
type %sGetResponse struct {
Message string `+"`json:\"message\"`"+`
Data []%s `+"`json:\"data\"`"+`
Meta %sMetaResponse `+"`json:\"meta\"`"+`
Summary *%sAggregateData `+"`json:\"summary,omitempty\"`"+`
}
`
`, data.Name, data.Name, data.Name, data.Name, nullablePrefix, nullablePrefix))
}
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\"`" + `
modelContent.WriteString(fmt.Sprintf("\n// Request struct for create\ntype %sCreateRequest struct {\n", data.Name))
for _, col := range createFields {
fieldName := snakeToPascal(col.Name)
_, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
jsonTag := snakeToCamel(col.Name)
var requestType string
if col.Nullable {
requestType = "*" + baseType
} else {
requestType = baseType
}
modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", fieldName, requestType, jsonTag))
}
modelContent.WriteString("}\n\n")
modelContent.WriteString(fmt.Sprintf(`// Response struct for create
type %sCreateResponse struct {
Message string `+"`json:\"message\"`"+`
Data *%s `+"`json:\"data\"`"+`
}
// Response struct untuk create
type ` + data.Name + `CreateResponse struct {
Message string ` + "`json:\"message\"`" + `
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
}
`
`, data.Name, data.Name))
}
if data.HasPut {
modelContent += `
// 1. Bangun string tag terlebih dahulu
idTagContent := `json:"-" validate:"required,uuid4"`
// 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\"`" + `
}
// 2. Gunakan strconv.Quote untuk membuatnya menjadi literal yang valid
// Hasilnya akan menjadi: "`json:\"-\" validate:\"required,uuid4\"`"
quotedIdTag := strconv.Quote(idTagContent)
// Response struct untuk update
type ` + data.Name + `UpdateResponse struct {
Message string ` + "`json:\"message\"`" + `
Data *` + data.Name + ` ` + "`json:\"data\"`" + `
}
`
// 3. Gunakan tag yang sudah di-"quote" dalam fmt.Sprintf
modelContent.WriteString(fmt.Sprintf("\n// Update request\ntype %sUpdateRequest struct {\n ID string %s\n", data.Name, quotedIdTag))
for _, col := range updateFields {
fieldName := snakeToPascal(col.Name)
_, baseType, _ := mapSQLTypeToGo(col.Type, col.Nullable, col.GoType)
jsonTag := snakeToCamel(col.Name)
var requestType string
if col.Nullable {
requestType = "*" + baseType
} else {
requestType = baseType
}
modelContent.WriteString(fmt.Sprintf(" %s %s `json:\"%s\"`\n", fieldName, requestType, jsonTag))
}
modelContent.WriteString("}\n\n")
modelContent.WriteString(fmt.Sprintf(`// Response struct for update
type %sUpdateResponse struct {
Message string `+"`json:\"message\"`"+`
Data *%s `+"`json:\"data\"`"+`
}
`, data.Name, data.Name))
}
if data.HasDelete {
modelContent += `
// Response struct untuk delete
type ` + data.Name + `DeleteResponse struct {
Message string ` + "`json:\"message\"`" + `
ID string ` + "`json:\"id\"`" + `
modelContent.WriteString(fmt.Sprintf(`// Response struct for delete
type %sDeleteResponse struct {
Message string `+"`json:\"message\"`"+`
ID string `+"`json:\"id\"`"+`
}
`
`, data.Name))
}
if data.HasFilter {
modelContent.WriteString(fmt.Sprintf("\n// Filter struct for query parameters\ntype %sFilter struct {\n", data.Name))
modelContent.WriteString(" Search *string `json:\"search,omitempty\" form:\"search\"`\n")
modelContent.WriteString(" DateFrom *time.Time `json:\"date_from,omitempty\" form:\"date_from\"`\n")
modelContent.WriteString(" DateTo *time.Time `json:\"date_to,omitempty\" form:\"date_to\"`\n")
for _, col := range data.TableSchema {
lowerName := strings.ToLower(col.Name)
if strings.Contains(lowerName, "status") {
modelContent.WriteString(fmt.Sprintf(" Status *string `json:\"status,omitempty\" form:\"%s\"`\n", col.Name))
}
}
modelContent.WriteString("}\n")
}
// Add filter struct
modelContent += `
// Filter struct untuk query parameters
type ` + data.Name + `Filter struct {
Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + `
Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + `
DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + `
DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + `
writeFile(modelFilePath, modelContent.String())
fmt.Printf("Successfully generated DYNAMIC model: %s\n", modelFileName)
}
`
writeFile(modelFilePath, modelContent)
fmt.Printf("Successfully generated model: %s\n", modelFileName)
// MODIFIKASI: Fungsi pembantu untuk memetakan tipe SQL ke Go
func mapSQLTypeToGo(sqlType string, nullable bool, explicitGoType string) (goType string, baseType string, valueType string) {
sqlType = strings.ToLower(sqlType)
if explicitGoType != "" {
baseType = explicitGoType
if nullable {
switch baseType {
case "string":
goType, valueType = "sql.NullString", "String"
case "int32":
goType, valueType = "sql.NullInt32", "Int32"
case "int64":
goType, valueType = "sql.NullInt64", "Int64"
case "bool":
goType, valueType = "sql.NullBool", "Bool"
case "float64":
goType, valueType = "sql.NullFloat64", "Float64"
case "time.Time":
goType, valueType = "sql.NullTime", "Time"
default:
goType, valueType = "sql.NullString", "String" // fallback
}
} else {
goType = baseType
}
return goType, baseType, valueType
}
switch {
case strings.Contains(sqlType, "serial") || sqlType == "int4":
if nullable {
return "sql.NullInt32", "int32", "Int32"
}
return "int32", "int32", ""
case sqlType == "int8" || sqlType == "bigint":
if nullable {
return "sql.NullInt64", "int64", "Int64"
}
return "int64", "int64", ""
case sqlType == "uuid":
if nullable {
return "sql.NullString", "string", "String"
}
return "string", "string", ""
case strings.Contains(sqlType, "varchar") || strings.Contains(sqlType, "text") || sqlType == "char":
if nullable {
return "sql.NullString", "string", "String"
}
return "string", "string", ""
case sqlType == "bool":
if nullable {
return "sql.NullBool", "bool", "Bool"
}
return "bool", "bool", ""
case strings.Contains(sqlType, "timestamp") || strings.Contains(sqlType, "date"):
if nullable {
return "sql.NullTime", "time.Time", "Time"
}
return "time.Time", "time.Time", ""
case strings.Contains(sqlType, "decimal") || strings.Contains(sqlType, "numeric") || sqlType == "float8" || sqlType == "real":
if nullable {
return "sql.NullFloat64", "float64", "Float64"
}
return "float64", "float64", ""
default:
if nullable {
return "sql.NullString", "string", "String"
}
return "string", "string", ""
}
}
func snakeToPascal(s string) string {
if s == "" {
return ""
}
// Pisahkan string berdasarkan underscore
parts := strings.Split(s, "_")
var pascal strings.Builder
for _, part := range parts {
if part == "" {
continue // Lewati bagian kosong jika ada double underscore
}
lowerPart := strings.ToLower(part)
// Handle common acronyms
if lowerPart == "id" {
pascal.WriteString("ID")
} else {
// Ubah huruf pertama menjadi kapital dan sisanya kecil
pascal.WriteString(strings.Title(lowerPart))
}
}
return pascal.String()
}
// snakeToCamel mengubah string dari snake_case menjadi camelCase.
// Contoh: "nama_hari" -> "namaHari"
func snakeToCamel(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
if len(parts) == 1 {
return strings.ToLower(parts[0])
}
// Bagian pertama: huruf pertama kecil
var camel strings.Builder
camel.WriteString(strings.ToLower(parts[0]))
// Bagian selanjutnya: huruf pertama besar (PascalCase)
for i := 1; i < len(parts); i++ {
if parts[i] == "" {
continue
}
camel.WriteString(strings.Title(strings.ToLower(parts[i])))
}
return camel.String()
}
// ================= ROUTES GENERATION =====================