first commit
This commit is contained in:
244
internal/utils/validation/duplicate.go
Normal file
244
internal/utils/validation/duplicate.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
queryUtils "api-service/internal/utils/query"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC VALIDATION RULE
|
||||
// =============================================================================
|
||||
|
||||
// ValidationRule mendefinisikan aturan untuk memeriksa duplikat atau kondisi lain.
|
||||
// Struct ini membuat validator dapat digunakan kembali untuk tabel apa pun.
|
||||
type ValidationRule struct {
|
||||
// TableName adalah nama tabel yang akan diperiksa.
|
||||
TableName string
|
||||
|
||||
// UniqueColumns adalah daftar kolom yang, jika digabungkan, harus unik.
|
||||
// Contoh: []string{"email"} atau []string{"first_name", "last_name", "dob"}
|
||||
UniqueColumns []string
|
||||
|
||||
// Conditions adalah filter tambahan yang harus dipenuhi.
|
||||
// Ini sangat berguna untuk aturan bisnis, seperti "status != 'deleted'".
|
||||
// Gunakan queryUtils.DynamicFilter untuk fleksibilitas penuh.
|
||||
Conditions []queryUtils.DynamicFilter
|
||||
|
||||
// ExcludeIDColumn dan ExcludeIDValue digunakan untuk operasi UPDATE,
|
||||
// untuk memastikan bahwa record tidak membandingkan dirinya sendiri.
|
||||
ExcludeIDColumn string
|
||||
ExcludeIDValue interface{}
|
||||
}
|
||||
|
||||
// NewUniqueFieldRule adalah helper untuk membuat aturan validasi unik untuk satu kolom.
|
||||
// Ini adalah cara cepat untuk membuat aturan yang paling umum.
|
||||
func NewUniqueFieldRule(tableName, uniqueColumn string, additionalConditions ...queryUtils.DynamicFilter) ValidationRule {
|
||||
return ValidationRule{
|
||||
TableName: tableName,
|
||||
UniqueColumns: []string{uniqueColumn},
|
||||
Conditions: additionalConditions,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC VALIDATOR
|
||||
// =============================================================================
|
||||
|
||||
// DynamicValidator menyediakan metode untuk menjalankan validasi berdasarkan ValidationRule.
|
||||
// Ini sepenuhnya generik dan tidak terikat pada tabel atau model tertentu.
|
||||
type DynamicValidator struct {
|
||||
qb *queryUtils.QueryBuilder
|
||||
}
|
||||
|
||||
// NewDynamicValidator membuat instance DynamicValidator baru.
|
||||
func NewDynamicValidator(qb *queryUtils.QueryBuilder) *DynamicValidator {
|
||||
return &DynamicValidator{qb: qb}
|
||||
}
|
||||
|
||||
// Validate menjalankan validasi terhadap aturan yang diberikan.
|
||||
// `data` adalah map yang berisi nilai untuk kolom yang akan diperiksa (biasanya dari request body).
|
||||
// Mengembalikan `true` jika ada duplikat yang ditemukan (validasi gagal), `false` jika tidak ada duplikat (validasi berhasil).
|
||||
func (dv *DynamicValidator) Validate(ctx context.Context, db *sqlx.DB, rule ValidationRule, data map[string]interface{}) (bool, error) {
|
||||
// LOGGING: Log validation start
|
||||
fmt.Printf("[VALIDATION] Starting validation for table: %s, unique columns: %v, data: %v\n", rule.TableName, rule.UniqueColumns, data)
|
||||
|
||||
if len(rule.UniqueColumns) == 0 {
|
||||
fmt.Printf("[VALIDATION] ERROR: ValidationRule must have at least one UniqueColumn\n")
|
||||
return false, fmt.Errorf("ValidationRule must have at least one UniqueColumn")
|
||||
}
|
||||
|
||||
// 1. Kumpulkan semua filter dari aturan
|
||||
var allFilters []queryUtils.DynamicFilter
|
||||
|
||||
// Tambahkan kondisi tambahan (misalnya, status != 'deleted')
|
||||
allFilters = append(allFilters, rule.Conditions...)
|
||||
fmt.Printf("[VALIDATION] Added %d condition filters\n", len(rule.Conditions))
|
||||
|
||||
// 2. Bangun filter untuk kolom unik berdasarkan data yang diberikan
|
||||
for _, colName := range rule.UniqueColumns {
|
||||
value, exists := data[colName]
|
||||
if !exists {
|
||||
// Jika data untuk kolom unik tidak ada, ini adalah kesalahan pemrograman.
|
||||
fmt.Printf("[VALIDATION] ERROR: data for unique column '%s' not found in provided data map\n", colName)
|
||||
return false, fmt.Errorf("data for unique column '%s' not found in provided data map", colName)
|
||||
}
|
||||
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||
Column: colName,
|
||||
Operator: queryUtils.OpEqual,
|
||||
Value: value,
|
||||
})
|
||||
fmt.Printf("[VALIDATION] Added filter for column '%s' with value: %v\n", colName, value)
|
||||
}
|
||||
|
||||
// 3. Tambahkan filter pengecualian ID (untuk operasi UPDATE)
|
||||
if rule.ExcludeIDColumn != "" {
|
||||
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||
Column: rule.ExcludeIDColumn,
|
||||
Operator: queryUtils.OpNotEqual,
|
||||
Value: rule.ExcludeIDValue,
|
||||
})
|
||||
fmt.Printf("[VALIDATION] Added exclude filter for column '%s' with value: %v\n", rule.ExcludeIDColumn, rule.ExcludeIDValue)
|
||||
}
|
||||
|
||||
// 4. Bangun dan eksekusi query untuk menghitung jumlah record yang cocok
|
||||
query := queryUtils.DynamicQuery{
|
||||
From: rule.TableName,
|
||||
Filters: []queryUtils.FilterGroup{{Filters: allFilters, LogicOp: "AND"}},
|
||||
}
|
||||
|
||||
fmt.Printf("[VALIDATION] Built query with %d total filters\n", len(allFilters))
|
||||
|
||||
count, err := dv.qb.ExecuteCount(ctx, db, query)
|
||||
if err != nil {
|
||||
fmt.Printf("[VALIDATION] ERROR: failed to execute validation query for table %s: %v\n", rule.TableName, err)
|
||||
return false, fmt.Errorf("failed to execute validation query for table %s: %w", rule.TableName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("[VALIDATION] Query executed successfully, count result: %d\n", count)
|
||||
|
||||
// 5. Kembalikan hasil
|
||||
result := count > 0
|
||||
fmt.Printf("[VALIDATION] Validation result: isDuplicate=%t (count > 0: %d > 0 = %t)\n", result, count, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH PENGGUNAAN (UNTUK DITEMPATKAN DI HANDLER ANDA)
|
||||
// =============================================================================
|
||||
|
||||
/*
|
||||
// --- Cara Penggunaan di RetribusiHandler ---
|
||||
|
||||
// 1. Tambahkan DynamicValidator ke struct handler
|
||||
type RetribusiHandler struct {
|
||||
// ...
|
||||
validator *validation.DynamicValidator
|
||||
}
|
||||
|
||||
// 2. Inisialisasi di constructor
|
||||
func NewRetribusiHandler() *RetribusiHandler {
|
||||
qb := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).SetAllowedColumns(...)
|
||||
|
||||
return &RetribusiHandler{
|
||||
// ...
|
||||
validator: validation.NewDynamicValidator(qb),
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Gunakan di CreateRetribusi
|
||||
func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) {
|
||||
var req retribusi.RetribusiCreateRequest
|
||||
// ... bind dan validasi request ...
|
||||
|
||||
// Siapkan aturan validasi: KodeTarif harus unik di antara record yang tidak dihapus.
|
||||
rule := validation.NewUniqueFieldRule(
|
||||
"data_retribusi", // Nama tabel
|
||||
"Kode_tarif", // Kolom yang harus unik
|
||||
queryUtils.DynamicFilter{ // Kondisi tambahan
|
||||
Column: "status",
|
||||
Operator: queryUtils.OpNotEqual,
|
||||
Value: "deleted",
|
||||
},
|
||||
)
|
||||
|
||||
// Siapkan data dari request untuk divalidasi
|
||||
dataToValidate := map[string]interface{}{
|
||||
"Kode_tarif": req.KodeTarif,
|
||||
}
|
||||
|
||||
// Eksekusi validasi
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if isDuplicate {
|
||||
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// ... lanjutkan proses create ...
|
||||
}
|
||||
|
||||
// 4. Gunakan di UpdateRetribusi
|
||||
func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req retribusi.RetribusiUpdateRequest
|
||||
// ... bind dan validasi request ...
|
||||
|
||||
// Siapkan aturan validasi: KodeTarif harus unik, kecuali untuk record dengan ID ini.
|
||||
rule := validation.ValidationRule{
|
||||
TableName: "data_retribusi",
|
||||
UniqueColumns: []string{"Kode_tarif"},
|
||||
Conditions: []queryUtils.DynamicFilter{
|
||||
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
|
||||
},
|
||||
ExcludeIDColumn: "id", // Kecualikan berdasarkan kolom 'id'
|
||||
ExcludeIDValue: id, // ...dengan nilai ID dari parameter
|
||||
}
|
||||
|
||||
dataToValidate := map[string]interface{}{
|
||||
"Kode_tarif": req.KodeTarif,
|
||||
}
|
||||
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if isDuplicate {
|
||||
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// ... lanjutkan proses update ...
|
||||
}
|
||||
|
||||
// --- Contoh Penggunaan untuk Kasus Lain ---
|
||||
|
||||
// Contoh: Validasi kombinasi unik untuk tabel 'users'
|
||||
// (email dan company_id harus unik bersama-sama)
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
// ...
|
||||
|
||||
rule := validation.ValidationRule{
|
||||
TableName: "users",
|
||||
UniqueColumns: []string{"email", "company_id"}, // Unik komposit
|
||||
}
|
||||
|
||||
dataToValidate := map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"company_id": req.CompanyID,
|
||||
}
|
||||
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
// ... handle error dan duplicate
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user