All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
837 lines
15 KiB
Go
837 lines
15 KiB
Go
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
|
|
// ---------------------------------------------------------
|
|
qInfo := ""
|
|
if table != nil {
|
|
qInfo = fmt.Sprintf(`
|
|
SELECT
|
|
table_name
|
|
FROM information_schema.tables
|
|
WHERE
|
|
table_schema = 'public'
|
|
AND table_type = 'BASE TABLE'
|
|
AND table_name = '%s'
|
|
ORDER BY table_name
|
|
`, *table)
|
|
} else {
|
|
qInfo = `
|
|
SELECT
|
|
table_name
|
|
FROM information_schema.tables
|
|
WHERE
|
|
table_schema = 'public'
|
|
AND table_type = 'BASE TABLE'
|
|
ORDER BY table_name
|
|
`
|
|
}
|
|
|
|
rows, err := sqlDB.Query(qInfo)
|
|
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,
|
|
"",
|
|
)
|
|
}
|