package migrations import ( "database/sql" "fmt" "log" "os" "path/filepath" "sort" "strings" "gorm.io/gen" "gorm.io/gen/field" "gorm.io/gorm" ) // ForeignKeyMeta menyimpan metadata Foreign Key dari PostgreSQL. type ForeignKeyMeta struct { ConstraintName string TableName string ColumnName string RefTable string RefColumn string Ordinal int } // GenerateModel membuat model GORM dari seluruh tabel public. func GenerateModel(table *string, db *gorm.DB) { currentDir, err := os.Getwd() if err != nil { log.Fatalf( "Gagal mendapatkan working directory: %v", err, ) } // Folder sementara untuk generated query. queryOutPath := filepath.Join( currentDir, "internal", "domain", "query_utils", ) // Folder model. modelOutPath := filepath.Join( currentDir, "internal", "domain", ) if err := os.MkdirAll(queryOutPath, os.ModePerm); err != nil { log.Fatalf( "Gagal membuat folder query_utils: %v", err, ) } if err := os.MkdirAll(modelOutPath, os.ModePerm); err != nil { log.Fatalf( "Gagal membuat folder domain: %v", err, ) } // --------------------------------------------------------- // SQL DB // --------------------------------------------------------- sqlDB, err := db.DB() if err != nil { log.Fatalf( "Gagal mengambil instance sql.DB: %v", err, ) } // --------------------------------------------------------- // Foreign Keys // --------------------------------------------------------- fkMetas := fetchForeignKeys(sqlDB) log.Printf( "Berhasil mendeteksi %d kolom Foreign Key fisik di database.", len(fkMetas), ) // --------------------------------------------------------- // GORM Gen // --------------------------------------------------------- g := gen.NewGenerator(gen.Config{ OutPath: queryOutPath, ModelPkgPath: modelOutPath, Mode: gen.WithoutContext, FieldWithIndexTag: true, FieldWithTypeTag: true, FieldNullable: true, FieldSignable: true, }) g.UseDB(db) // --------------------------------------------------------- // Get tables // --------------------------------------------------------- rows, err := sqlDB.Query(` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name `) if err != nil { log.Fatalf( "Gagal mengambil daftar tabel: %v", err, ) } defer rows.Close() var tableNames []string for rows.Next() { var tName string if err := rows.Scan(&tName); err != nil { log.Printf( "Gagal membaca nama tabel: %v", err, ) continue } // Jangan generate tabel internal GORM. if strings.HasPrefix(tName, "gorm_") { continue } tableNames = append( tableNames, tName, ) } if err := rows.Err(); err != nil { log.Fatalf( "Gagal membaca daftar tabel: %v", err, ) } log.Printf( "Berhasil menemukan %d tabel.", len(tableNames), ) // --------------------------------------------------------- // Group FK // // Satu constraint = satu relationship. // // Ini penting untuk composite FK. // --------------------------------------------------------- fkGroups := groupForeignKeys(fkMetas) // --------------------------------------------------------- // Relationship options per table // --------------------------------------------------------- tableRelationOpts := make( map[string][]gen.ModelOpt, ) // Digunakan untuk mencegah relationship yang benar-benar // identik dibuat lebih dari satu kali. // // key: // // source table // relation type // property // target table // foreign keys // references // seenRelations := make( map[string]bool, ) // --------------------------------------------------------- // Build relationships // --------------------------------------------------------- for _, group := range fkGroups { if len(group) == 0 { continue } // Pastikan urutan composite FK konsisten. sort.Slice( group, func(i, j int) bool { return group[i].Ordinal < group[j].Ordinal }, ) first := group[0] // Pastikan source dan target table memang ada. if !containsString( tableNames, first.TableName, ) { log.Printf( "WARNING: source table %s tidak ditemukan.", first.TableName, ) continue } if !containsString( tableNames, first.RefTable, ) { log.Printf( "WARNING: referenced table %s tidak ditemukan.", first.RefTable, ) continue } // ----------------------------------------------------- // Collect FK columns // ----------------------------------------------------- foreignKeys := make( []string, 0, len(group), ) references := make( []string, 0, len(group), ) for _, fk := range group { foreignKeys = append( foreignKeys, fk.ColumnName, ) references = append( references, fk.RefColumn, ) } // ----------------------------------------------------- // BELONGS TO // ----------------------------------------------------- belongsProperty := belongsToPropertyName(first) belongsRelationKey := buildRelationKey( first.TableName, "belongs_to", belongsProperty, first.RefTable, foreignKeys, references, ) if !seenRelations[belongsRelationKey] { seenRelations[belongsRelationKey] = true log.Printf( "BELONGS TO: %s (%s) -> %s (%s) [%s]", first.TableName, strings.Join(foreignKeys, ", "), first.RefTable, strings.Join(references, ", "), belongsProperty, ) relation := gen.FieldRelate( field.BelongsTo, belongsProperty, g.GenerateModel(first.RefTable), &field.RelateConfig{ GORMTag: field.GormTag{ "foreignKey": foreignKeys, "references": references, }, }, ) tableRelationOpts[first.TableName] = append( tableRelationOpts[first.TableName], relation, ) } // ----------------------------------------------------- // HAS MANY // ----------------------------------------------------- hasManyProperty := hasManyPropertyName(first) hasManyRelationKey := buildRelationKey( first.RefTable, "has_many", hasManyProperty, first.TableName, foreignKeys, references, ) if !seenRelations[hasManyRelationKey] { seenRelations[hasManyRelationKey] = true log.Printf( "HAS MANY: %s -> %s (%s) [%s]", first.RefTable, first.TableName, strings.Join(foreignKeys, ", "), hasManyProperty, ) relation := gen.FieldRelate( field.HasMany, hasManyProperty, g.GenerateModel(first.TableName), &field.RelateConfig{ GORMTag: field.GormTag{ "foreignKey": foreignKeys, "references": references, }, }, ) tableRelationOpts[first.RefTable] = append( tableRelationOpts[first.RefTable], relation, ) } } // --------------------------------------------------------- // Generate final models // --------------------------------------------------------- var finalModels []any // Simpan generated model berdasarkan table. // // Kita tidak menggunakan gen.Model karena type tersebut // memang tidak diekspos oleh gorm/gen. // // GenerateModel tetap dipanggil di sini untuk menghasilkan // final model dengan relation options. // // Tidak ada kebutuhan untuk menyimpan return type-nya. for _, tName := range tableNames { opts := tableRelationOpts[tName] compiledModel := g.GenerateModel( tName, opts..., ) finalModels = append( finalModels, compiledModel, ) } // --------------------------------------------------------- // ApplyBasic // --------------------------------------------------------- if table != nil && *table != "" { targetTable := strings.TrimSpace(*table) if !containsString( tableNames, targetTable, ) { log.Fatalf( "Tabel %s tidak ditemukan di database!", targetTable, ) } // Generate model khusus table yang diminta. specModel := g.GenerateModel( targetTable, tableRelationOpts[targetTable]..., ) g.ApplyBasic(specModel) log.Printf( "Hanya generate model: %s", targetTable, ) } else { g.ApplyBasic( finalModels..., ) log.Printf( "Generate seluruh %d model.", len(finalModels), ) } // --------------------------------------------------------- // Execute // --------------------------------------------------------- g.Execute() // --------------------------------------------------------- // Remove query utility // --------------------------------------------------------- if err := os.RemoveAll(queryOutPath); err != nil { log.Printf( "Peringatan: gagal menghapus query_utils: %v", err, ) } // --------------------------------------------------------- // Rename .gen.go -> .go // --------------------------------------------------------- if err := renameGeneratedFiles(modelOutPath); err != nil { log.Printf( "Peringatan: gagal mengubah nama file model: %v", err, ) } log.Println( "Selesai! Model berhasil dibuat.", ) } // ------------------------------------------------------------- // Group Foreign Keys // ------------------------------------------------------------- // // Satu constraint_name dianggap sebagai satu relationship. // // Contoh: // // FK: // company_id -> company.id // user_id -> user.id // // Jika berbeda constraint: // // fk_order_company // fk_order_user // // maka akan menjadi dua relationship. // // Composite FK: // // FK (company_id, order_id) // -> orders (company_id, id) // // menjadi satu relationship. // // ------------------------------------------------------------- func groupForeignKeys( metas []ForeignKeyMeta, ) [][]ForeignKeyMeta { groups := make( map[string][]ForeignKeyMeta, ) for _, meta := range metas { key := fmt.Sprintf( "%s|%s", meta.TableName, meta.ConstraintName, ) groups[key] = append( groups[key], meta, ) } result := make( [][]ForeignKeyMeta, 0, len(groups), ) for _, group := range groups { sort.Slice( group, func(i, j int) bool { return group[i].Ordinal < group[j].Ordinal }, ) result = append( result, group, ) } // Untuk deterministic output. sort.Slice( result, func(i, j int) bool { if result[i][0].TableName != result[j][0].TableName { return result[i][0].TableName < result[j][0].TableName } return result[i][0].ConstraintName < result[j][0].ConstraintName }, ) return result } // ------------------------------------------------------------- // Build unique relation key // ------------------------------------------------------------- func buildRelationKey( sourceTable string, relationType string, propertyName string, targetTable string, foreignKeys []string, references []string, ) string { return strings.Join( []string{ sourceTable, relationType, propertyName, targetTable, strings.Join(foreignKeys, ","), strings.Join(references, ","), }, "|", ) } // ------------------------------------------------------------- // BelongsTo property name // ------------------------------------------------------------- // // user_id // -> User_Relation // // customer_id // -> Customer_Relation // // company_id // -> Company_Relation // // ------------------------------------------------------------- func belongsToPropertyName(fk ForeignKeyMeta) string { name := strings.TrimSuffix( fk.ColumnName, "_id", ) if name == "" || name == fk.ColumnName { name = fk.RefTable } return snakeToPascalCase(name) + "_Relation" } // ------------------------------------------------------------- // HasMany property name // ------------------------------------------------------------- // // orders -> Orders_Relation // // ------------------------------------------------------------- func hasManyPropertyName(fk ForeignKeyMeta) string { tableName := snakeToPascalCase(fk.TableName) columnName := strings.TrimSuffix( fk.ColumnName, "_id", ) columnName = snakeToPascalCase(columnName) return tableName + "_By" + columnName + "_Relation" } // ------------------------------------------------------------- // Fetch Foreign Keys // ------------------------------------------------------------- func fetchForeignKeys( db *sql.DB, ) []ForeignKeyMeta { query := ` SELECT tc.constraint_name, kcu.table_name, kcu.column_name, ccu.table_name AS referenced_table_name, ccu.column_name AS referenced_column_name, kcu.ordinal_position FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.referential_constraints AS rc ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema JOIN information_schema.key_column_usage AS ccu ON rc.unique_constraint_name = ccu.constraint_name AND rc.unique_constraint_schema = ccu.table_schema AND kcu.ordinal_position = ccu.ordinal_position WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' ORDER BY kcu.table_name, tc.constraint_name, kcu.ordinal_position ` rows, err := db.Query(query) if err != nil { log.Printf( "Gagal membaca metadata Foreign Key: %v", err, ) return nil } defer rows.Close() var metas []ForeignKeyMeta for rows.Next() { var m ForeignKeyMeta err := rows.Scan( &m.ConstraintName, &m.TableName, &m.ColumnName, &m.RefTable, &m.RefColumn, &m.Ordinal, ) if err != nil { log.Printf( "Gagal membaca metadata Foreign Key: %v", err, ) continue } metas = append( metas, m, ) } if err := rows.Err(); err != nil { log.Printf( "Error membaca Foreign Key rows: %v", err, ) } return metas } // ------------------------------------------------------------- // Rename generated files // ------------------------------------------------------------- func renameGeneratedFiles( dir string, ) error { files, err := os.ReadDir(dir) if err != nil { return err } for _, file := range files { if file.IsDir() { continue } if !strings.HasSuffix( file.Name(), ".gen.go", ) { continue } oldPath := filepath.Join( dir, file.Name(), ) newName := strings.TrimSuffix( file.Name(), ".gen.go", ) + ".go" newPath := filepath.Join( dir, newName, ) if err := os.Rename( oldPath, newPath, ); err != nil { return err } } return nil } // ------------------------------------------------------------- // Contains string // ------------------------------------------------------------- func containsString( items []string, target string, ) bool { for _, item := range items { if item == target { return true } } return false } // ------------------------------------------------------------- // snake_case -> PascalCase // ------------------------------------------------------------- func snakeToPascalCase( s string, ) string { parts := strings.Split( s, "_", ) for i, part := range parts { if len(part) == 0 { continue } parts[i] = strings.ToUpper(part[:1]) + part[1:] } return strings.Join( parts, "", ) }