perbaikan tool generete
This commit is contained in:
@@ -80,9 +80,11 @@ tools/generate.bat product get post put delete
|
|||||||
./tools/generate.sh product get post put delete
|
./tools/generate.sh product get post put delete
|
||||||
|
|
||||||
# Atau langsung dengan Go
|
# Atau langsung dengan Go
|
||||||
go run tools/generate-handler.go product get post
|
go run tools/generate-handler.go orders get post
|
||||||
|
|
||||||
go run tools/generate-handler.go order get post put delete stats
|
go run tools/generate-handler.go orders/product get post
|
||||||
|
|
||||||
|
go run tools/generate-handler.go orders/order get post put delete dynamic search stats
|
||||||
|
|
||||||
go run tools/generate-bpjs-handler.go reference/peserta get
|
go run tools/generate-bpjs-handler.go reference/peserta get
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
models "api-service/internal/models/retribusi"
|
models "api-service/internal/models"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
85
internal/models/models.go
Normal file
85
internal/models/models.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
|
||||||
|
type NullableInt32 struct {
|
||||||
|
Int32 int32 `json:"int32,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the sql.Scanner interface for NullableInt32
|
||||||
|
func (n *NullableInt32) Scan(value interface{}) error {
|
||||||
|
var ni sql.NullInt32
|
||||||
|
if err := ni.Scan(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.Int32 = ni.Int32
|
||||||
|
n.Valid = ni.Valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface for NullableInt32
|
||||||
|
func (n NullableInt32) Value() (driver.Value, error) {
|
||||||
|
if !n.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return n.Int32, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata untuk pagination - dioptimalkan
|
||||||
|
type MetaResponse struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
HasNext bool `json:"has_next"`
|
||||||
|
HasPrev bool `json:"has_prev"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate data untuk summary
|
||||||
|
type AggregateData struct {
|
||||||
|
TotalActive int `json:"total_active"`
|
||||||
|
TotalDraft int `json:"total_draft"`
|
||||||
|
TotalInactive int `json:"total_inactive"`
|
||||||
|
ByStatus map[string]int `json:"by_status"`
|
||||||
|
ByDinas map[string]int `json:"by_dinas,omitempty"`
|
||||||
|
ByJenis map[string]int `json:"by_jenis,omitempty"`
|
||||||
|
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
||||||
|
CreatedToday int `json:"created_today"`
|
||||||
|
UpdatedToday int `json:"updated_today"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response yang konsisten
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation constants
|
||||||
|
const (
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusActive = "active"
|
||||||
|
StatusInactive = "inactive"
|
||||||
|
StatusDeleted = "deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidStatuses untuk validasi
|
||||||
|
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
|
||||||
|
|
||||||
|
// IsValidStatus helper function
|
||||||
|
func IsValidStatus(status string) bool {
|
||||||
|
for _, validStatus := range ValidStatuses {
|
||||||
|
if status == validStatus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,307 +1,229 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"api-service/internal/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
|
|
||||||
type NullableInt32 struct {
|
|
||||||
Int32 int32 `json:"int32,omitempty"`
|
|
||||||
Valid bool `json:"valid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan implements the sql.Scanner interface for NullableInt32
|
|
||||||
func (n *NullableInt32) Scan(value interface{}) error {
|
|
||||||
var ni sql.NullInt32
|
|
||||||
if err := ni.Scan(value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n.Int32 = ni.Int32
|
|
||||||
n.Valid = ni.Valid
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value implements the driver.Valuer interface for NullableInt32
|
|
||||||
func (n NullableInt32) Value() (driver.Value, error) {
|
|
||||||
if !n.Valid {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return n.Int32, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retribusi represents the data structure for the retribusi table
|
// Retribusi represents the data structure for the retribusi table
|
||||||
// with proper null handling and optimized JSON marshaling
|
// with proper null handling and optimized JSON marshaling
|
||||||
type Retribusi struct {
|
type Retribusi struct {
|
||||||
ID string `json:"id" db:"id"`
|
ID string `json:"id" db:"id"`
|
||||||
Status string `json:"status" db:"status"`
|
Status string `json:"status" db:"status"`
|
||||||
Sort NullableInt32 `json:"sort,omitempty" db:"sort"`
|
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
|
||||||
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
|
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
|
||||||
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
|
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
|
||||||
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
|
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
|
||||||
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
|
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
|
||||||
Jenis sql.NullString `json:"jenis,omitempty" db:"Jenis"`
|
Jenis sql.NullString `json:"jenis,omitempty" db:"Jenis"`
|
||||||
Pelayanan sql.NullString `json:"pelayanan,omitempty" db:"Pelayanan"`
|
Pelayanan sql.NullString `json:"pelayanan,omitempty" db:"Pelayanan"`
|
||||||
Dinas sql.NullString `json:"dinas,omitempty" db:"Dinas"`
|
Dinas sql.NullString `json:"dinas,omitempty" db:"Dinas"`
|
||||||
KelompokObyek sql.NullString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
|
KelompokObyek sql.NullString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
|
||||||
KodeTarif sql.NullString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
|
KodeTarif sql.NullString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
|
||||||
Tarif sql.NullString `json:"tarif,omitempty" db:"Tarif"`
|
Tarif sql.NullString `json:"tarif,omitempty" db:"Tarif"`
|
||||||
Satuan sql.NullString `json:"satuan,omitempty" db:"Satuan"`
|
Satuan sql.NullString `json:"satuan,omitempty" db:"Satuan"`
|
||||||
TarifOvertime sql.NullString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
|
TarifOvertime sql.NullString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
|
||||||
SatuanOvertime sql.NullString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
|
SatuanOvertime sql.NullString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
|
||||||
RekeningPokok sql.NullString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
|
RekeningPokok sql.NullString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
|
||||||
RekeningDenda sql.NullString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
|
RekeningDenda sql.NullString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
|
||||||
Uraian1 sql.NullString `json:"uraian_1,omitempty" db:"Uraian_1"`
|
Uraian1 sql.NullString `json:"uraian_1,omitempty" db:"Uraian_1"`
|
||||||
Uraian2 sql.NullString `json:"uraian_2,omitempty" db:"Uraian_2"`
|
Uraian2 sql.NullString `json:"uraian_2,omitempty" db:"Uraian_2"`
|
||||||
Uraian3 sql.NullString `json:"uraian_3,omitempty" db:"Uraian_3"`
|
Uraian3 sql.NullString `json:"uraian_3,omitempty" db:"Uraian_3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response
|
// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response
|
||||||
func (r Retribusi) MarshalJSON() ([]byte, error) {
|
func (r Retribusi) MarshalJSON() ([]byte, error) {
|
||||||
type Alias Retribusi
|
type Alias Retribusi
|
||||||
aux := &struct {
|
aux := &struct {
|
||||||
Sort *int `json:"sort,omitempty"`
|
Sort *int `json:"sort,omitempty"`
|
||||||
UserCreated *string `json:"user_created,omitempty"`
|
UserCreated *string `json:"user_created,omitempty"`
|
||||||
DateCreated *time.Time `json:"date_created,omitempty"`
|
DateCreated *time.Time `json:"date_created,omitempty"`
|
||||||
UserUpdated *string `json:"user_updated,omitempty"`
|
UserUpdated *string `json:"user_updated,omitempty"`
|
||||||
DateUpdated *time.Time `json:"date_updated,omitempty"`
|
DateUpdated *time.Time `json:"date_updated,omitempty"`
|
||||||
Jenis *string `json:"jenis,omitempty"`
|
Jenis *string `json:"jenis,omitempty"`
|
||||||
Pelayanan *string `json:"pelayanan,omitempty"`
|
Pelayanan *string `json:"pelayanan,omitempty"`
|
||||||
Dinas *string `json:"dinas,omitempty"`
|
Dinas *string `json:"dinas,omitempty"`
|
||||||
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
|
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
|
||||||
KodeTarif *string `json:"kode_tarif,omitempty"`
|
KodeTarif *string `json:"kode_tarif,omitempty"`
|
||||||
Tarif *string `json:"tarif,omitempty"`
|
Tarif *string `json:"tarif,omitempty"`
|
||||||
Satuan *string `json:"satuan,omitempty"`
|
Satuan *string `json:"satuan,omitempty"`
|
||||||
TarifOvertime *string `json:"tarif_overtime,omitempty"`
|
TarifOvertime *string `json:"tarif_overtime,omitempty"`
|
||||||
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
|
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
|
||||||
RekeningPokok *string `json:"rekening_pokok,omitempty"`
|
RekeningPokok *string `json:"rekening_pokok,omitempty"`
|
||||||
RekeningDenda *string `json:"rekening_denda,omitempty"`
|
RekeningDenda *string `json:"rekening_denda,omitempty"`
|
||||||
Uraian1 *string `json:"uraian_1,omitempty"`
|
Uraian1 *string `json:"uraian_1,omitempty"`
|
||||||
Uraian2 *string `json:"uraian_2,omitempty"`
|
Uraian2 *string `json:"uraian_2,omitempty"`
|
||||||
Uraian3 *string `json:"uraian_3,omitempty"`
|
Uraian3 *string `json:"uraian_3,omitempty"`
|
||||||
*Alias
|
*Alias
|
||||||
}{
|
}{
|
||||||
Alias: (*Alias)(&r),
|
Alias: (*Alias)(&r),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert NullableInt32 to pointer
|
// Convert NullableInt32 to pointer
|
||||||
if r.Sort.Valid {
|
if r.Sort.Valid {
|
||||||
sort := int(r.Sort.Int32)
|
sort := int(r.Sort.Int32)
|
||||||
aux.Sort = &sort
|
aux.Sort = &sort
|
||||||
}
|
}
|
||||||
if r.UserCreated.Valid {
|
if r.UserCreated.Valid {
|
||||||
aux.UserCreated = &r.UserCreated.String
|
aux.UserCreated = &r.UserCreated.String
|
||||||
}
|
}
|
||||||
if r.DateCreated.Valid {
|
if r.DateCreated.Valid {
|
||||||
aux.DateCreated = &r.DateCreated.Time
|
aux.DateCreated = &r.DateCreated.Time
|
||||||
}
|
}
|
||||||
if r.UserUpdated.Valid {
|
if r.UserUpdated.Valid {
|
||||||
aux.UserUpdated = &r.UserUpdated.String
|
aux.UserUpdated = &r.UserUpdated.String
|
||||||
}
|
}
|
||||||
if r.DateUpdated.Valid {
|
if r.DateUpdated.Valid {
|
||||||
aux.DateUpdated = &r.DateUpdated.Time
|
aux.DateUpdated = &r.DateUpdated.Time
|
||||||
}
|
}
|
||||||
if r.Jenis.Valid {
|
if r.Jenis.Valid {
|
||||||
aux.Jenis = &r.Jenis.String
|
aux.Jenis = &r.Jenis.String
|
||||||
}
|
}
|
||||||
if r.Pelayanan.Valid {
|
if r.Pelayanan.Valid {
|
||||||
aux.Pelayanan = &r.Pelayanan.String
|
aux.Pelayanan = &r.Pelayanan.String
|
||||||
}
|
}
|
||||||
if r.Dinas.Valid {
|
if r.Dinas.Valid {
|
||||||
aux.Dinas = &r.Dinas.String
|
aux.Dinas = &r.Dinas.String
|
||||||
}
|
}
|
||||||
if r.KelompokObyek.Valid {
|
if r.KelompokObyek.Valid {
|
||||||
aux.KelompokObyek = &r.KelompokObyek.String
|
aux.KelompokObyek = &r.KelompokObyek.String
|
||||||
}
|
}
|
||||||
if r.KodeTarif.Valid {
|
if r.KodeTarif.Valid {
|
||||||
aux.KodeTarif = &r.KodeTarif.String
|
aux.KodeTarif = &r.KodeTarif.String
|
||||||
}
|
}
|
||||||
if r.Tarif.Valid {
|
if r.Tarif.Valid {
|
||||||
aux.Tarif = &r.Tarif.String
|
aux.Tarif = &r.Tarif.String
|
||||||
}
|
}
|
||||||
if r.Satuan.Valid {
|
if r.Satuan.Valid {
|
||||||
aux.Satuan = &r.Satuan.String
|
aux.Satuan = &r.Satuan.String
|
||||||
}
|
}
|
||||||
if r.TarifOvertime.Valid {
|
if r.TarifOvertime.Valid {
|
||||||
aux.TarifOvertime = &r.TarifOvertime.String
|
aux.TarifOvertime = &r.TarifOvertime.String
|
||||||
}
|
}
|
||||||
if r.SatuanOvertime.Valid {
|
if r.SatuanOvertime.Valid {
|
||||||
aux.SatuanOvertime = &r.SatuanOvertime.String
|
aux.SatuanOvertime = &r.SatuanOvertime.String
|
||||||
}
|
}
|
||||||
if r.RekeningPokok.Valid {
|
if r.RekeningPokok.Valid {
|
||||||
aux.RekeningPokok = &r.RekeningPokok.String
|
aux.RekeningPokok = &r.RekeningPokok.String
|
||||||
}
|
}
|
||||||
if r.RekeningDenda.Valid {
|
if r.RekeningDenda.Valid {
|
||||||
aux.RekeningDenda = &r.RekeningDenda.String
|
aux.RekeningDenda = &r.RekeningDenda.String
|
||||||
}
|
}
|
||||||
if r.Uraian1.Valid {
|
if r.Uraian1.Valid {
|
||||||
aux.Uraian1 = &r.Uraian1.String
|
aux.Uraian1 = &r.Uraian1.String
|
||||||
}
|
}
|
||||||
if r.Uraian2.Valid {
|
if r.Uraian2.Valid {
|
||||||
aux.Uraian2 = &r.Uraian2.String
|
aux.Uraian2 = &r.Uraian2.String
|
||||||
}
|
}
|
||||||
if r.Uraian3.Valid {
|
if r.Uraian3.Valid {
|
||||||
aux.Uraian3 = &r.Uraian3.String
|
aux.Uraian3 = &r.Uraian3.String
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(aux)
|
return json.Marshal(aux)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods untuk mendapatkan nilai yang aman
|
// Helper methods untuk mendapatkan nilai yang aman
|
||||||
func (r *Retribusi) GetJenis() string {
|
func (r *Retribusi) GetJenis() string {
|
||||||
if r.Jenis.Valid {
|
if r.Jenis.Valid {
|
||||||
return r.Jenis.String
|
return r.Jenis.String
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Retribusi) GetDinas() string {
|
func (r *Retribusi) GetDinas() string {
|
||||||
if r.Dinas.Valid {
|
if r.Dinas.Valid {
|
||||||
return r.Dinas.String
|
return r.Dinas.String
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Retribusi) GetTarif() string {
|
func (r *Retribusi) GetTarif() string {
|
||||||
if r.Tarif.Valid {
|
if r.Tarif.Valid {
|
||||||
return r.Tarif.String
|
return r.Tarif.String
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk GET by ID - diperbaiki struktur
|
// Response struct untuk GET by ID - diperbaiki struktur
|
||||||
type RetribusiGetByIDResponse struct {
|
type RetribusiGetByIDResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data *Retribusi `json:"data"`
|
Data *Retribusi `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request struct untuk create - dioptimalkan dengan validasi
|
// Request struct untuk create - dioptimalkan dengan validasi
|
||||||
type RetribusiCreateRequest struct {
|
type RetribusiCreateRequest struct {
|
||||||
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||||
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
|
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
|
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
|
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
|
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
|
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Uraian1 *string `json:"uraian_1,omitempty"`
|
Uraian1 *string `json:"uraian_1,omitempty"`
|
||||||
Uraian2 *string `json:"uraian_2,omitempty"`
|
Uraian2 *string `json:"uraian_2,omitempty"`
|
||||||
Uraian3 *string `json:"uraian_3,omitempty"`
|
Uraian3 *string `json:"uraian_3,omitempty"`
|
||||||
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
|
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
|
||||||
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
|
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
|
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
|
||||||
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
|
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
|
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
|
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk create
|
// Response struct untuk create
|
||||||
type RetribusiCreateResponse struct {
|
type RetribusiCreateResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data *Retribusi `json:"data"`
|
Data *Retribusi `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update request - sama seperti create tapi dengan ID
|
// Update request - sama seperti create tapi dengan ID
|
||||||
type RetribusiUpdateRequest struct {
|
type RetribusiUpdateRequest struct {
|
||||||
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
|
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
|
||||||
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||||
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
|
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
|
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
|
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
|
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
|
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Uraian1 *string `json:"uraian_1,omitempty"`
|
Uraian1 *string `json:"uraian_1,omitempty"`
|
||||||
Uraian2 *string `json:"uraian_2,omitempty"`
|
Uraian2 *string `json:"uraian_2,omitempty"`
|
||||||
Uraian3 *string `json:"uraian_3,omitempty"`
|
Uraian3 *string `json:"uraian_3,omitempty"`
|
||||||
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
|
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
|
||||||
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
|
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
|
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
|
||||||
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
|
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
|
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
|
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk update
|
// Response struct untuk update
|
||||||
type RetribusiUpdateResponse struct {
|
type RetribusiUpdateResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data *Retribusi `json:"data"`
|
Data *Retribusi `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk delete
|
// Response struct untuk delete
|
||||||
type RetribusiDeleteResponse struct {
|
type RetribusiDeleteResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced GET response dengan pagination dan aggregation
|
// Enhanced GET response dengan pagination dan aggregation
|
||||||
type RetribusiGetResponse struct {
|
type RetribusiGetResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data []Retribusi `json:"data"`
|
Data []Retribusi `json:"data"`
|
||||||
Meta MetaResponse `json:"meta"`
|
Meta models.MetaResponse `json:"meta"`
|
||||||
Summary *AggregateData `json:"summary,omitempty"`
|
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata untuk pagination - dioptimalkan
|
|
||||||
type MetaResponse struct {
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
TotalPages int `json:"total_pages"`
|
|
||||||
CurrentPage int `json:"current_page"`
|
|
||||||
HasNext bool `json:"has_next"`
|
|
||||||
HasPrev bool `json:"has_prev"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate data untuk summary
|
|
||||||
type AggregateData struct {
|
|
||||||
TotalActive int `json:"total_active"`
|
|
||||||
TotalDraft int `json:"total_draft"`
|
|
||||||
TotalInactive int `json:"total_inactive"`
|
|
||||||
ByStatus map[string]int `json:"by_status"`
|
|
||||||
ByDinas map[string]int `json:"by_dinas,omitempty"`
|
|
||||||
ByJenis map[string]int `json:"by_jenis,omitempty"`
|
|
||||||
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
|
||||||
CreatedToday int `json:"created_today"`
|
|
||||||
UpdatedToday int `json:"updated_today"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error response yang konsisten
|
|
||||||
type ErrorResponse struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter struct untuk query parameters
|
// Filter struct untuk query parameters
|
||||||
type RetribusiFilter struct {
|
type RetribusiFilter struct {
|
||||||
Status *string `json:"status,omitempty" form:"status"`
|
Status *string `json:"status,omitempty" form:"status"`
|
||||||
Jenis *string `json:"jenis,omitempty" form:"jenis"`
|
Jenis *string `json:"jenis,omitempty" form:"jenis"`
|
||||||
Dinas *string `json:"dinas,omitempty" form:"dinas"`
|
Dinas *string `json:"dinas,omitempty" form:"dinas"`
|
||||||
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
|
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
|
||||||
Search *string `json:"search,omitempty" form:"search"`
|
Search *string `json:"search,omitempty" form:"search"`
|
||||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||||
}
|
|
||||||
|
|
||||||
// Validation constants
|
|
||||||
const (
|
|
||||||
StatusDraft = "draft"
|
|
||||||
StatusActive = "active"
|
|
||||||
StatusInactive = "inactive"
|
|
||||||
StatusDeleted = "deleted"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidStatuses untuk validasi
|
|
||||||
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
|
|
||||||
|
|
||||||
// IsValidStatus helper function
|
|
||||||
func IsValidStatus(status string) bool {
|
|
||||||
for _, validStatus := range ValidStatuses {
|
|
||||||
if status == validStatus {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,18 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
// BPJS endpoints
|
// BPJS endpoints
|
||||||
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||||
v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
|
v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
|
||||||
|
// Retribusi endpoints
|
||||||
|
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||||
|
retribusiGroup := v1.Group("/retribusi")
|
||||||
|
{
|
||||||
|
retribusiGroup.GET("", retribusiHandler.GetRetribusi)
|
||||||
|
retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru
|
||||||
|
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian
|
||||||
|
retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID)
|
||||||
|
retribusiGroup.POST("", retribusiHandler.CreateRetribusi)
|
||||||
|
retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi)
|
||||||
|
retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi)
|
||||||
|
}
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PROTECTED ROUTES (Authentication Required)
|
// PROTECTED ROUTES (Authentication Required)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -68,15 +79,15 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
protected.GET("/auth/me", authHandler.Me)
|
protected.GET("/auth/me", authHandler.Me)
|
||||||
|
|
||||||
// Retribusi endpoints (CRUD operations - should be protected)
|
// Retribusi endpoints (CRUD operations - should be protected)
|
||||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||||
protectedRetribusi := protected.Group("/retribusi")
|
// protectedRetribusi := protected.Group("/retribusi")
|
||||||
{
|
// {
|
||||||
protectedRetribusi.GET("/", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi/
|
// protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi
|
||||||
protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
|
// protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
|
||||||
protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
|
// protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
|
||||||
protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
|
// protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
|
||||||
protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
|
// protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
|
||||||
}
|
// }
|
||||||
|
|
||||||
// BPJS endpoints (sensitive data - should be protected)
|
// BPJS endpoints (sensitive data - should be protected)
|
||||||
// bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
// bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetEnv retrieves environment variable with fallback
|
|
||||||
func GetEnv(key, defaultValue string) string {
|
|
||||||
if value := os.Getenv(key); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnvAsInt retrieves environment variable as integer with fallback
|
|
||||||
func GetEnvAsInt(key string, defaultValue int) int {
|
|
||||||
valueStr := GetEnv(key, "")
|
|
||||||
if value, err := strconv.Atoi(valueStr); err == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnvAsBool retrieves environment variable as boolean with fallback
|
|
||||||
func GetEnvAsBool(key string, defaultValue bool) bool {
|
|
||||||
valueStr := GetEnv(key, "")
|
|
||||||
if value, err := strconv.ParseBool(valueStr); err == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnvAsDuration retrieves environment variable as duration with fallback
|
|
||||||
func GetEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
|
|
||||||
valueStr := GetEnv(key, "")
|
|
||||||
if value, err := time.ParseDuration(valueStr); err == nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
541
internal/utils/filters/dynamic_filter.go
Normal file
541
internal/utils/filters/dynamic_filter.go
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterOperator represents supported filter operators
|
||||||
|
type FilterOperator string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpEqual FilterOperator = "_eq"
|
||||||
|
OpNotEqual FilterOperator = "_neq"
|
||||||
|
OpLike FilterOperator = "_like"
|
||||||
|
OpILike FilterOperator = "_ilike"
|
||||||
|
OpIn FilterOperator = "_in"
|
||||||
|
OpNotIn FilterOperator = "_nin"
|
||||||
|
OpGreaterThan FilterOperator = "_gt"
|
||||||
|
OpGreaterThanEqual FilterOperator = "_gte"
|
||||||
|
OpLessThan FilterOperator = "_lt"
|
||||||
|
OpLessThanEqual FilterOperator = "_lte"
|
||||||
|
OpBetween FilterOperator = "_between"
|
||||||
|
OpNotBetween FilterOperator = "_nbetween"
|
||||||
|
OpNull FilterOperator = "_null"
|
||||||
|
OpNotNull FilterOperator = "_nnull"
|
||||||
|
OpContains FilterOperator = "_contains"
|
||||||
|
OpNotContains FilterOperator = "_ncontains"
|
||||||
|
OpStartsWith FilterOperator = "_starts_with"
|
||||||
|
OpEndsWith FilterOperator = "_ends_with"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DynamicFilter represents a single filter condition
|
||||||
|
type DynamicFilter struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Operator FilterOperator `json:"operator"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
LogicOp string `json:"logic_op,omitempty"` // AND, OR
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterGroup represents a group of filters
|
||||||
|
type FilterGroup struct {
|
||||||
|
Filters []DynamicFilter `json:"filters"`
|
||||||
|
LogicOp string `json:"logic_op"` // AND, OR
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicQuery represents the complete query structure
|
||||||
|
type DynamicQuery struct {
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
Filters []FilterGroup `json:"filters,omitempty"`
|
||||||
|
Sort []SortField `json:"sort,omitempty"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
GroupBy []string `json:"group_by,omitempty"`
|
||||||
|
Having []FilterGroup `json:"having,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortField represents sorting configuration
|
||||||
|
type SortField struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Order string `json:"order"` // ASC, DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryBuilder builds SQL queries from dynamic filters
|
||||||
|
type QueryBuilder struct {
|
||||||
|
tableName string
|
||||||
|
columnMapping map[string]string // Maps API field names to DB column names
|
||||||
|
allowedColumns map[string]bool // Security: only allow specified columns
|
||||||
|
paramCounter int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryBuilder creates a new query builder instance
|
||||||
|
func NewQueryBuilder(tableName string) *QueryBuilder {
|
||||||
|
return &QueryBuilder{
|
||||||
|
tableName: tableName,
|
||||||
|
columnMapping: make(map[string]string),
|
||||||
|
allowedColumns: make(map[string]bool),
|
||||||
|
paramCounter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColumnMapping sets the mapping between API field names and database column names
|
||||||
|
func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilder {
|
||||||
|
qb.columnMapping = mapping
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAllowedColumns sets the list of allowed columns for security
|
||||||
|
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
|
||||||
|
qb.allowedColumns = make(map[string]bool)
|
||||||
|
for _, col := range columns {
|
||||||
|
qb.allowedColumns[col] = true
|
||||||
|
}
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildQuery builds the complete SQL query
|
||||||
|
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
|
qb.paramCounter = 0
|
||||||
|
|
||||||
|
// Build SELECT clause
|
||||||
|
selectClause := qb.buildSelectClause(query.Fields)
|
||||||
|
|
||||||
|
// Build FROM clause
|
||||||
|
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ORDER BY clause
|
||||||
|
orderClause := qb.buildOrderClause(query.Sort)
|
||||||
|
|
||||||
|
// Build GROUP BY clause
|
||||||
|
groupClause := qb.buildGroupByClause(query.GroupBy)
|
||||||
|
|
||||||
|
// Build HAVING clause
|
||||||
|
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{selectClause, fromClause}
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupClause != "" {
|
||||||
|
sqlParts = append(sqlParts, groupClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
||||||
|
args = append(args, havingArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if orderClause != "" {
|
||||||
|
sqlParts = append(sqlParts, orderClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pagination
|
||||||
|
if query.Limit > 0 {
|
||||||
|
qb.paramCounter++
|
||||||
|
sqlParts = append(sqlParts, fmt.Sprintf("LIMIT $%d", qb.paramCounter))
|
||||||
|
args = append(args, query.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Offset > 0 {
|
||||||
|
qb.paramCounter++
|
||||||
|
sqlParts = append(sqlParts, fmt.Sprintf("OFFSET $%d", qb.paramCounter))
|
||||||
|
args = append(args, query.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSelectClause builds the SELECT part of the query
|
||||||
|
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
||||||
|
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
|
||||||
|
return "SELECT *"
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedFields []string
|
||||||
|
for _, field := range fields {
|
||||||
|
if field == "*.*" || field == "*" {
|
||||||
|
selectedFields = append(selectedFields, "*")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map field name if mapping exists
|
||||||
|
if mappedCol, exists := qb.columnMapping[field]; exists {
|
||||||
|
field = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check: only allow specified columns
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[field] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, field))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(selectedFields) == 0 {
|
||||||
|
return "SELECT *"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SELECT " + strings.Join(selectedFields, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWhereClause builds the WHERE part of the query
|
||||||
|
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(filterGroups) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var conditions []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
for i, group := range filterGroups {
|
||||||
|
groupCondition, groupArgs, err := qb.buildFilterGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupCondition != "" {
|
||||||
|
if i > 0 {
|
||||||
|
logicOp := "AND"
|
||||||
|
if group.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(group.LogicOp)
|
||||||
|
}
|
||||||
|
conditions = append(conditions, logicOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, "("+groupCondition+")")
|
||||||
|
args = append(args, groupArgs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(conditions, " "), args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFilterGroup builds conditions for a filter group
|
||||||
|
func (qb *QueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(group.Filters) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var conditions []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
for i, filter := range group.Filters {
|
||||||
|
condition, filterArgs, err := qb.buildFilterCondition(filter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition != "" {
|
||||||
|
if i > 0 {
|
||||||
|
logicOp := "AND"
|
||||||
|
if filter.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(filter.LogicOp)
|
||||||
|
} else if group.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(group.LogicOp)
|
||||||
|
}
|
||||||
|
conditions = append(conditions, logicOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, condition)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(conditions, " "), args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFilterCondition builds a single filter condition
|
||||||
|
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []interface{}, error) {
|
||||||
|
// Map column name if mapping exists
|
||||||
|
column := filter.Column
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
||||||
|
return "", nil, fmt.Errorf("column '%s' is not allowed", filter.Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap column name in quotes for PostgreSQL
|
||||||
|
column = fmt.Sprintf(`"%s"`, column)
|
||||||
|
|
||||||
|
switch filter.Operator {
|
||||||
|
case OpEqual:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s = $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpNotEqual:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s != $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLike:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s LIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpILike:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpIn:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders []string
|
||||||
|
var args []interface{}
|
||||||
|
for _, val := range values {
|
||||||
|
qb.paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
|
||||||
|
args = append(args, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
|
||||||
|
|
||||||
|
case OpNotIn:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders []string
|
||||||
|
var args []interface{}
|
||||||
|
for _, val := range values {
|
||||||
|
qb.paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
|
||||||
|
args = append(args, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s NOT IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
|
||||||
|
|
||||||
|
case OpGreaterThan:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s > $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpGreaterThanEqual:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s >= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLessThan:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s < $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLessThanEqual:
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s <= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpBetween:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("between operator requires exactly 2 values")
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
param1 := qb.paramCounter
|
||||||
|
qb.paramCounter++
|
||||||
|
param2 := qb.paramCounter
|
||||||
|
return fmt.Sprintf("%s BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
|
||||||
|
|
||||||
|
case OpNotBetween:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("not between operator requires exactly 2 values")
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
param1 := qb.paramCounter
|
||||||
|
qb.paramCounter++
|
||||||
|
param2 := qb.paramCounter
|
||||||
|
return fmt.Sprintf("%s NOT BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
|
||||||
|
|
||||||
|
case OpNull:
|
||||||
|
return fmt.Sprintf("%s IS NULL", column), nil, nil
|
||||||
|
|
||||||
|
case OpNotNull:
|
||||||
|
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
|
||||||
|
|
||||||
|
case OpContains:
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpNotContains:
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s NOT ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpStartsWith:
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpEndsWith:
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported operator: %s", filter.Operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseArrayValue parses array values from various formats
|
||||||
|
func (qb *QueryBuilder) parseArrayValue(value interface{}) []interface{} {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already a slice
|
||||||
|
if reflect.TypeOf(value).Kind() == reflect.Slice {
|
||||||
|
v := reflect.ValueOf(value)
|
||||||
|
result := make([]interface{}, v.Len())
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
result[i] = v.Index(i).Interface()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a string, try to split by comma
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if strings.Contains(str, ",") {
|
||||||
|
parts := strings.Split(str, ",")
|
||||||
|
result := make([]interface{}, len(parts))
|
||||||
|
for i, part := range parts {
|
||||||
|
result[i] = strings.TrimSpace(part)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return []interface{}{str}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []interface{}{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOrderClause builds the ORDER BY clause
|
||||||
|
func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
|
||||||
|
if len(sortFields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderParts []string
|
||||||
|
for _, sort := range sortFields {
|
||||||
|
column := sort.Column
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
order := "ASC"
|
||||||
|
if sort.Order != "" {
|
||||||
|
order = strings.ToUpper(sort.Order)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderParts = append(orderParts, fmt.Sprintf(`"%s" %s`, column, order))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(orderParts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ORDER BY " + strings.Join(orderParts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGroupByClause builds the GROUP BY clause
|
||||||
|
func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string {
|
||||||
|
if len(groupFields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupParts []string
|
||||||
|
for _, field := range groupFields {
|
||||||
|
column := field
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groupParts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "GROUP BY " + strings.Join(groupParts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHavingClause builds the HAVING clause
|
||||||
|
func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(havingGroups) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.buildWhereClause(havingGroups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCountQuery builds a count query
|
||||||
|
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
|
qb.paramCounter = 0
|
||||||
|
|
||||||
|
// Build FROM clause
|
||||||
|
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build GROUP BY clause
|
||||||
|
groupClause := qb.buildGroupByClause(query.GroupBy)
|
||||||
|
|
||||||
|
// Build HAVING clause
|
||||||
|
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine parts
|
||||||
|
sqlParts := []string{"SELECT COUNT(*)", fromClause}
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupClause != "" {
|
||||||
|
sqlParts = append(sqlParts, groupClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
||||||
|
args = append(args, havingArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
241
internal/utils/filters/query_parser.go
Normal file
241
internal/utils/filters/query_parser.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryParser parses HTTP query parameters into DynamicQuery
|
||||||
|
type QueryParser struct {
|
||||||
|
defaultLimit int
|
||||||
|
maxLimit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryParser creates a new query parser
|
||||||
|
func NewQueryParser() *QueryParser {
|
||||||
|
return &QueryParser{
|
||||||
|
defaultLimit: 10,
|
||||||
|
maxLimit: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLimits sets default and maximum limits
|
||||||
|
func (qp *QueryParser) SetLimits(defaultLimit, maxLimit int) *QueryParser {
|
||||||
|
qp.defaultLimit = defaultLimit
|
||||||
|
qp.maxLimit = maxLimit
|
||||||
|
return qp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseQuery parses URL query parameters into DynamicQuery
|
||||||
|
func (qp *QueryParser) ParseQuery(values url.Values) (DynamicQuery, error) {
|
||||||
|
query := DynamicQuery{
|
||||||
|
Limit: qp.defaultLimit,
|
||||||
|
Offset: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse fields
|
||||||
|
if fields := values.Get("fields"); fields != "" {
|
||||||
|
if fields == "*.*" || fields == "*" {
|
||||||
|
query.Fields = []string{"*"}
|
||||||
|
} else {
|
||||||
|
query.Fields = strings.Split(fields, ",")
|
||||||
|
for i, field := range query.Fields {
|
||||||
|
query.Fields[i] = strings.TrimSpace(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pagination
|
||||||
|
if limit := values.Get("limit"); limit != "" {
|
||||||
|
if l, err := strconv.Atoi(limit); err == nil {
|
||||||
|
if l > 0 && l <= qp.maxLimit {
|
||||||
|
query.Limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset := values.Get("offset"); offset != "" {
|
||||||
|
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
|
||||||
|
query.Offset = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filters
|
||||||
|
filters, err := qp.parseFilters(values)
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
query.Filters = filters
|
||||||
|
|
||||||
|
// Parse sorting
|
||||||
|
sorts, err := qp.parseSorting(values)
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
query.Sort = sorts
|
||||||
|
|
||||||
|
// Parse group by
|
||||||
|
if groupBy := values.Get("group"); groupBy != "" {
|
||||||
|
query.GroupBy = strings.Split(groupBy, ",")
|
||||||
|
for i, field := range query.GroupBy {
|
||||||
|
query.GroupBy[i] = strings.TrimSpace(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilters parses filter parameters
|
||||||
|
// Supports format: filter[column][operator]=value
|
||||||
|
func (qp *QueryParser) parseFilters(values url.Values) ([]FilterGroup, error) {
|
||||||
|
filterMap := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
// Group filters by column
|
||||||
|
for key, vals := range values {
|
||||||
|
if strings.HasPrefix(key, "filter[") && strings.HasSuffix(key, "]") {
|
||||||
|
// Parse filter[column][operator] format
|
||||||
|
parts := strings.Split(key[7:len(key)-1], "][")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
column := parts[0]
|
||||||
|
operator := parts[1]
|
||||||
|
|
||||||
|
if filterMap[column] == nil {
|
||||||
|
filterMap[column] = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vals) > 0 {
|
||||||
|
filterMap[column][operator] = vals[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filterMap) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to FilterGroup
|
||||||
|
var filters []DynamicFilter
|
||||||
|
|
||||||
|
for column, operators := range filterMap {
|
||||||
|
for opStr, value := range operators {
|
||||||
|
operator := FilterOperator(opStr)
|
||||||
|
|
||||||
|
// Parse value based on operator
|
||||||
|
var parsedValue interface{}
|
||||||
|
switch operator {
|
||||||
|
case OpIn, OpNotIn:
|
||||||
|
if value != "" {
|
||||||
|
parsedValue = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
case OpBetween, OpNotBetween:
|
||||||
|
if value != "" {
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
parsedValue = []interface{}{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case OpNull, OpNotNull:
|
||||||
|
parsedValue = nil
|
||||||
|
default:
|
||||||
|
parsedValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = append(filters, DynamicFilter{
|
||||||
|
Column: column,
|
||||||
|
Operator: operator,
|
||||||
|
Value: parsedValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filters) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []FilterGroup{{
|
||||||
|
Filters: filters,
|
||||||
|
LogicOp: "AND",
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSorting parses sort parameters
|
||||||
|
// Supports format: sort=column1,-column2 (- for DESC)
|
||||||
|
func (qp *QueryParser) parseSorting(values url.Values) ([]SortField, error) {
|
||||||
|
sortParam := values.Get("sort")
|
||||||
|
if sortParam == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorts []SortField
|
||||||
|
fields := strings.Split(sortParam, ",")
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
order := "ASC"
|
||||||
|
column := field
|
||||||
|
|
||||||
|
if strings.HasPrefix(field, "-") {
|
||||||
|
order = "DESC"
|
||||||
|
column = field[1:]
|
||||||
|
} else if strings.HasPrefix(field, "+") {
|
||||||
|
column = field[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
sorts = append(sorts, SortField{
|
||||||
|
Column: column,
|
||||||
|
Order: order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAdvancedFilters parses complex filter structures
|
||||||
|
// Supports nested filters and logic operators
|
||||||
|
func (qp *QueryParser) ParseAdvancedFilters(filterParam string) ([]FilterGroup, error) {
|
||||||
|
// This would be for more complex JSON-based filters
|
||||||
|
// Implementation depends on your specific needs
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse date values
|
||||||
|
func parseDate(value string) (interface{}, error) {
|
||||||
|
// Try different date formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05.000Z",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.Parse(format, value); err == nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse numeric values
|
||||||
|
func parseNumeric(value string) interface{} {
|
||||||
|
// Try integer first
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try float
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
<?php
|
|
||||||
if (!function_exists('formCreateData')) {
|
|
||||||
function formCreateData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noKartu" => $data['noKartu'],
|
|
||||||
"tglSep" => $data['tglSep'],
|
|
||||||
"ppkPelayanan" => $data['ppkPelayanan'],
|
|
||||||
"jnsPelayanan" => $data['jnsPelayanan'],
|
|
||||||
"klsRawat" => [
|
|
||||||
"klsRawatHak" => $data['klsRawatHak'],
|
|
||||||
"klsRawatNaik" => $data['klsRawatNaik'],
|
|
||||||
"pembiayaan" => $data['pembiayaan'],
|
|
||||||
"penanggungJawab" => $data['penanggungJawab']
|
|
||||||
],
|
|
||||||
"noMR" => $data['noMR'],
|
|
||||||
"rujukan" => [
|
|
||||||
"asalRujukan" => $data['asalRujukan'],
|
|
||||||
"tglRujukan" => $data['tglRujukan'],
|
|
||||||
"noRujukan" => $data['noRujukan'],
|
|
||||||
"ppkRujukan" => $data['ppkRujukan']
|
|
||||||
],
|
|
||||||
"catatan" => $data['catatan'],
|
|
||||||
"diagAwal" => $data['diagAwal'],
|
|
||||||
"poli" => [
|
|
||||||
"tujuan" => $data['tujuan'],
|
|
||||||
"eksekutif" => $data['eksekutif']
|
|
||||||
],
|
|
||||||
"cob" => [
|
|
||||||
"cob" => $data['cob']
|
|
||||||
],
|
|
||||||
"katarak" => [
|
|
||||||
"katarak" => $data['katarak']
|
|
||||||
],
|
|
||||||
"jaminan" => [
|
|
||||||
"lakaLantas" => $data['lakaLantas'],
|
|
||||||
"noLP" => $data['noLP'],
|
|
||||||
"penjamin" => [
|
|
||||||
"tglKejadian" => $data['tglKejadian'],
|
|
||||||
"keterangan" => $data['keterangan'],
|
|
||||||
"suplesi" => [
|
|
||||||
"suplesi" => $data['suplesi'],
|
|
||||||
"noSepSuplesi" => $data['noSepSuplesi'],
|
|
||||||
"lokasiLaka" => [
|
|
||||||
"kdPropinsi" => $data['kdPropinsi'],
|
|
||||||
"kdKabupaten" => $data['kdKabupaten'],
|
|
||||||
"kdKecamatan" => $data['kdKecamatan']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"tujuanKunj" => $data['tujuanKunj'],
|
|
||||||
"flagProcedure" => $data['flagProcedure'],
|
|
||||||
"kdPenunjang" => $data['kdPenunjang'],
|
|
||||||
"assesmentPel" => $data['assesmentPel'],
|
|
||||||
"skdp" => [
|
|
||||||
"noSurat" => $data['noSurat'],
|
|
||||||
"kodeDPJP" => $data['kodeDPJP']
|
|
||||||
],
|
|
||||||
"dpjpLayan" => $data['dpjpLayan'],
|
|
||||||
"noTelp" => $data['noTelp'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formUpdateData')) {
|
|
||||||
|
|
||||||
|
|
||||||
function formUpdateData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noSep" => $data['noSep'],
|
|
||||||
"klsRawat" => [
|
|
||||||
"klsRawatHak" => $data['klsRawatHak'],
|
|
||||||
"klsRawatNaik" => $data['klsRawatNaik'],
|
|
||||||
"pembiayaan" => $data['pembiayaan'],
|
|
||||||
"penanggungJawab" => $data['penanggungJawab']
|
|
||||||
],
|
|
||||||
"noMR" => $data['noMR'],
|
|
||||||
"catatan" => $data['catatan'],
|
|
||||||
"diagAwal" => $data['diagAwal'],
|
|
||||||
"poli" => [
|
|
||||||
"tujuan" => $data['tujuan'],
|
|
||||||
"eksekutif" => $data['eksekutif']
|
|
||||||
],
|
|
||||||
"cob" => [
|
|
||||||
"cob" => $data['cob']
|
|
||||||
],
|
|
||||||
"katarak" => [
|
|
||||||
"katarak" => $data['katarak']
|
|
||||||
],
|
|
||||||
"jaminan" => [
|
|
||||||
"lakaLantas" => $data['lakaLantas'],
|
|
||||||
"penjamin" => [
|
|
||||||
"tglKejadian" => $data['tglKejadian'],
|
|
||||||
"keterangan" => $data['keterangan'],
|
|
||||||
"suplesi" => [
|
|
||||||
"suplesi" => $data['suplesi'],
|
|
||||||
"noSepSuplesi" => $data['noSepSuplesi'],
|
|
||||||
"lokasiLaka" => [
|
|
||||||
"kdPropinsi" => $data['kdPropinsi'],
|
|
||||||
"kdKabupaten" => $data['kdKabupaten'],
|
|
||||||
"kdKecamatan" => $data['kdKecamatan']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"dpjpLayan" => $data['dpjpLayan'],
|
|
||||||
"noTelp" => $data['noTelp'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formDeleteData')) {
|
|
||||||
function formDeleteData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noSep" => $data['noSep'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formPengajuanData')) {
|
|
||||||
function formPengajuanData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noKartu" => $data['noKartu'],
|
|
||||||
"tglSep" => $data['tglSep'],
|
|
||||||
"jnsPelayanan" => $data['jnsPelayanan'],
|
|
||||||
"jnsPengajuan" => $data['jnsPengajuan'],
|
|
||||||
"keterangan" => $data['keterangan'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formAprovalPengajuanData')) {
|
|
||||||
function formAprovalPengajuanData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noKartu" => $data['noKartu'],
|
|
||||||
"tglSep" => $data['tglSep'],
|
|
||||||
"jnsPelayanan" => $data['jnsPelayanan'],
|
|
||||||
"jnsPengajuan" => $data['jnsPengajuan'],
|
|
||||||
"keterangan" => $data['keterangan'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formTanggalPulangData')) {
|
|
||||||
function formTanggalPulangData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noSep" => $data['noSep'],
|
|
||||||
"statusPulang" => $data['statusPulang'],
|
|
||||||
"noSuratMeninggal" => $data['noSuratMeninggal'],
|
|
||||||
"tglMeninggal" => $data['tglMeninggal'],
|
|
||||||
"tglPulang" => $data['tglPulang'],
|
|
||||||
"noLPManual" => $data['noLPManual'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formDeleteSepinternalData')) {
|
|
||||||
function formDeleteSepinternalData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noSep" => $data['noSep'],
|
|
||||||
"noSurat" => $data['noSurat'],
|
|
||||||
"tglRujukanInternal" => $data['tglRujukanInternal'],
|
|
||||||
"kdPoliTuj" => $data['kdPoliTuj'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!function_exists('formRandomAnswerData')) {
|
|
||||||
function formRandomAnswerData($data = [])
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"request" => [
|
|
||||||
"t_sep" => [
|
|
||||||
"noKartu" => $data['noKartu'],
|
|
||||||
"tglSep" => $data['tglSep'],
|
|
||||||
"jenPel" => $data['jenPel'],
|
|
||||||
"ppkPelSep" => $data['ppkPelSep'],
|
|
||||||
"tglLahir" => $data['tglLahir'],
|
|
||||||
"ppkPst" => $data['ppkPst'],
|
|
||||||
"user" => $data['user']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
141
internal/utils/validation/duplicate_validator.go
Normal file
141
internal/utils/validation/duplicate_validator.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationConfig holds configuration for duplicate validation
|
||||||
|
type ValidationConfig struct {
|
||||||
|
TableName string
|
||||||
|
IDColumn string
|
||||||
|
StatusColumn string
|
||||||
|
DateColumn string
|
||||||
|
ActiveStatuses []string
|
||||||
|
AdditionalFields map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DuplicateValidator provides methods for validating duplicate entries
|
||||||
|
type DuplicateValidator struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDuplicateValidator creates a new instance of DuplicateValidator
|
||||||
|
func NewDuplicateValidator(db *sql.DB) *DuplicateValidator {
|
||||||
|
return &DuplicateValidator{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDuplicate checks for duplicate entries based on the provided configuration
|
||||||
|
func (dv *DuplicateValidator) ValidateDuplicate(ctx context.Context, config ValidationConfig, identifier interface{}) error {
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM %s
|
||||||
|
WHERE %s = $1
|
||||||
|
AND %s = ANY($2)
|
||||||
|
AND DATE(%s) = CURRENT_DATE
|
||||||
|
`, config.TableName, config.IDColumn, config.StatusColumn, config.DateColumn)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := dv.db.QueryRowContext(ctx, query, identifier, config.ActiveStatuses).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check duplicate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("data with ID %v already exists with active status today", identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDuplicateWithCustomFields checks for duplicates with additional custom fields
|
||||||
|
func (dv *DuplicateValidator) ValidateDuplicateWithCustomFields(ctx context.Context, config ValidationConfig, fields map[string]interface{}) error {
|
||||||
|
whereClause := fmt.Sprintf("%s = ANY($1) AND DATE(%s) = CURRENT_DATE", config.StatusColumn, config.DateColumn)
|
||||||
|
args := []interface{}{config.ActiveStatuses}
|
||||||
|
argIndex := 2
|
||||||
|
|
||||||
|
// Add additional field conditions
|
||||||
|
for fieldName, fieldValue := range config.AdditionalFields {
|
||||||
|
whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||||
|
args = append(args, fieldValue)
|
||||||
|
argIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dynamic fields
|
||||||
|
for fieldName, fieldValue := range fields {
|
||||||
|
whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||||
|
args = append(args, fieldValue)
|
||||||
|
argIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s", config.TableName, whereClause)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := dv.db.QueryRowContext(ctx, query, args...).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check duplicate with custom fields: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("duplicate entry found with the specified criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateOncePerDay ensures only one submission per day for a given identifier
|
||||||
|
func (dv *DuplicateValidator) ValidateOncePerDay(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) error {
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM %s
|
||||||
|
WHERE %s = $1
|
||||||
|
AND DATE(%s) = CURRENT_DATE
|
||||||
|
`, tableName, idColumn, dateColumn)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check daily submission: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("only one submission allowed per day for ID %v", identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastSubmissionTime returns the last submission time for a given identifier
|
||||||
|
func (dv *DuplicateValidator) GetLastSubmissionTime(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) (*time.Time, error) {
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT %s
|
||||||
|
FROM %s
|
||||||
|
WHERE %s = $1
|
||||||
|
ORDER BY %s DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, dateColumn, tableName, idColumn, dateColumn)
|
||||||
|
|
||||||
|
var lastTime time.Time
|
||||||
|
err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&lastTime)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil // No previous submission
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get last submission time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lastTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRetribusiConfig returns default configuration for retribusi validation
|
||||||
|
func DefaultRetribusiConfig() ValidationConfig {
|
||||||
|
return ValidationConfig{
|
||||||
|
TableName: "data_retribusi",
|
||||||
|
IDColumn: "id",
|
||||||
|
StatusColumn: "status",
|
||||||
|
DateColumn: "date_created",
|
||||||
|
ActiveStatuses: []string{"active", "draft"},
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user