Add Dynamic Filter
Some checks failed
Go-test / build (push) Has been cancelled

This commit is contained in:
2025-09-11 15:18:44 +07:00
parent 4b12c49455
commit 4a0488c352
9 changed files with 951 additions and 418 deletions

View File

@@ -251,7 +251,7 @@ func (h *QrisHandler) GetQrisByIP(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.getQrisByIP(ctx, dbConn, ip)
items, err := h.getQrisByIP(ctx, dbConn, ip, c)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Qris not found", err, http.StatusNotFound)
@@ -263,7 +263,7 @@ func (h *QrisHandler) GetQrisByIP(c *gin.Context) {
response := qris.QrisGetByIPResponse{
Message: "qris details retrieved successfully",
Data: item,
Data: items,
}
c.JSON(http.StatusOK, response)
@@ -304,7 +304,7 @@ func (h *QrisHandler) GetQrisStats(c *gin.Context) {
// Database operations
func (h *QrisHandler) getQrisByID(ctx context.Context, dbConn *sql.DB, id int) (*qris.Qris, error) {
query := "SELECT id, status, created_at, updated_at, display_name, display_amount, qrvalue, ip, display_nobill, posdevice FROM t_qrdata WHERE id = $1 AND status != '0'"
query := "SELECT id, status, created_at, updated_at, display_name, display_amount, qrvalue, ip, display_nobill, posdevice FROM t_qrdata WHERE id = $1 AND status = '1'"
row := dbConn.QueryRowContext(ctx, query, id)
var item qris.Qris
@@ -316,17 +316,24 @@ func (h *QrisHandler) getQrisByID(ctx context.Context, dbConn *sql.DB, id int) (
return &item, nil
}
func (h *QrisHandler) getQrisByIP(ctx context.Context, dbConn *sql.DB, ip string) (*qris.Qris, error) {
query := "SELECT id, status, created_at, updated_at, display_name, display_amount, qrvalue, ip, display_nobill, posdevice FROM t_qrdata WHERE ip = $1 AND status != '0'"
row := dbConn.QueryRowContext(ctx, query, ip)
func (h *QrisHandler) getQrisByIP(ctx context.Context, dbConn *sql.DB, ip string, c *gin.Context) ([]qris.Qris, error) {
// Use QrisFilter to filter by IP
filter := qris.QrisFilter{
IPAddress: &ip,
}
var item qris.Qris
err := row.Scan(&item.ID, &item.Status, &item.CreatedAt, &item.UpdatedAt, &item.DisplayName, &item.DisplayAmount, &item.QrValue, &item.IPAddress, &item.DisplayNoBill, &item.PosDevice)
// Parse pagination parameters from query, default to 10, 0
limit, offset, err := h.parsePaginationParams(c)
if err != nil {
return nil, err
}
return &item, nil
// Use fetchQris to get all rows matching the IP
items, err := h.fetchQris(ctx, dbConn, filter, limit, offset)
if err != nil {
return nil, err
}
return items, nil
}
/*func (h *QrisHandler) createQris(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) (*qris.Qris, error) {
@@ -383,9 +390,13 @@ func (h *QrisHandler) getQrisByIP(ctx context.Context, dbConn *sql.DB, ip string
func (h *QrisHandler) fetchQris(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, limit, offset int) ([]qris.Qris, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf("SELECT id, status, created_at, updated_at, display_name, display_amount, qrvalue, ip, display_nobill, posdevice FROM t_qrdata WHERE %s ORDER BY created_at DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
query := fmt.Sprintf("SELECT id, posdevice, ip, invoice_number, qrvalue, status, created_at, updated_at, display_nobill, display_name, display_amount FROM t_qrdata WHERE %s ORDER BY created_at DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
args = append(args, limit, offset)
// Log the query and arguments
log.Printf("[DEBUG] SQL Query: %s", query)
log.Printf("[DEBUG] SQL Args: %#v", args)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch qris query failed: %w", err)
@@ -416,15 +427,16 @@ func (h *QrisHandler) scanQris(rows *sql.Rows) (qris.Qris, error) {
// Scan into individual fields to handle nullable types properly
err := rows.Scan(
&item.ID,
&item.PosDevice,
&item.IPAddress,
&item.InvoiceNumber,
&item.QrValue,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
&item.DisplayNoBill,
&item.DisplayName,
&item.DisplayAmount,
&item.QrValue,
&item.IPAddress,
&item.DisplayNoBill,
&item.PosDevice,
)
return item, err
@@ -433,6 +445,11 @@ func (h *QrisHandler) scanQris(rows *sql.Rows) (qris.Qris, error) {
func (h *QrisHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM t_qrdata WHERE %s", whereClause)
// Log the query and arguments
log.Printf("[DEBUG] SQL Count Query: %s", countQuery)
log.Printf("[DEBUG] SQL Count Args: %#v", args)
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
return fmt.Errorf("total count query failed: %w", err)
@@ -608,6 +625,18 @@ func (h *QrisHandler) parseFilterParams(c *gin.Context) qris.QrisFilter {
}
}
if ip := c.Query("ip"); ip != "" {
filter.IPAddress = &ip
}
if display_name := c.Query("display_name"); display_name != "" {
filter.DisplayName = &display_name
}
if posdevice := c.Query("posdevice"); posdevice != "" {
filter.PosDevice = &posdevice
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
@@ -630,7 +659,7 @@ func (h *QrisHandler) parseFilterParams(c *gin.Context) qris.QrisFilter {
// Build WHERE clause dengan filter parameters
func (h *QrisHandler) buildWhereClause(filter qris.QrisFilter) (string, []interface{}) {
conditions := []string{"status != '0'"}
conditions := []string{"status = '1'"}
args := []interface{}{}
paramCount := 1
@@ -640,7 +669,37 @@ func (h *QrisHandler) buildWhereClause(filter qris.QrisFilter) (string, []interf
paramCount++
}
if filter.IPAddress != nil {
conditions = append(conditions, fmt.Sprintf(`"ip" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.IPAddress+"%")
paramCount++
}
if filter.DisplayName != nil {
conditions = append(conditions, fmt.Sprintf(`"display_name" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.DisplayName+"%")
paramCount++
}
if filter.PosDevice != nil {
conditions = append(conditions, fmt.Sprintf(`"posdevice" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.PosDevice+"%")
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf(`(
"ip" ILIKE $%d OR
"display_name" ILIKE $%d OR
"posdevice" ILIKE $%d
)`, paramCount, paramCount, paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
/*if filter.Search != nil {
searchTerm := "%" + *filter.Search + "%"
searchFields := []string{"ip", "display_name", "posdevice"}
searchConditions := make([]string, 0, len(searchFields))
@@ -651,7 +710,7 @@ func (h *QrisHandler) buildWhereClause(filter qris.QrisFilter) (string, []interf
}
// Combine with OR
conditions = append(conditions, fmt.Sprintf("(%s)", strings.Join(searchConditions, " OR ")))
}
}*/
if filter.DateFrom != nil {
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", paramCount))
@@ -755,107 +814,128 @@ func (h *QrisHandler) calculateMeta(limit, offset, total int) models.MetaRespons
// fetchDynamic executes dynamic query
func (h *QrisHandler) fetchDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]qris.Qris, int, error) {
// Setup query builder
builder := utils.NewQueryBuilder("t_qrdata").
countBuilder := utils.NewQueryBuilder("t_qrdata").
SetColumnMapping(map[string]string{
"id": "id",
"posdevice": "posdevice",
"ip": "ip",
"invoice_number": "invoice_number",
"qrvalue": "qrvalue",
"status": "status",
"created_at": "created_at",
"updated_at": "updated_at",
"display_nobill": "display_nobill",
"display_name": "display_name",
"display_amount": "display_amount",
"qrvalue": "qrvalue",
"ip": "ip",
"display_nobill": "display_nobill",
"posdevice": "posdevice",
}).
SetAllowedColumns([]string{
"id", "status", "created_at", "updated_at", "display_name",
"display_amount", "qrvalue", "ip", "display_nobill", "posdevice",
"id",
"posdevice",
"ip",
"invoice_number",
"qrvalue",
"status",
"created_at",
"updated_at",
"display_nobill",
"display_name",
"display_amount",
})
mainBuilder := utils.NewQueryBuilder("t_qrdata").
SetColumnMapping(map[string]string{
"id": "id",
"posdevice": "posdevice",
"ip": "ip",
"invoice_number": "invoice_number",
"qrvalue": "qrvalue",
"status": "status",
"created_at": "created_at",
"updated_at": "updated_at",
"display_nobill": "display_nobill",
"display_name": "display_name",
"display_amount": "display_amount",
}).
SetAllowedColumns([]string{
"id",
"posdevice",
"ip",
"invoice_number",
"qrvalue",
"status",
"created_at",
"updated_at",
"display_nobill",
"display_name",
"display_amount",
})
// Add default filter to exclude deleted records
query.Filters = append([]utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpNotEqual,
Value: "0",
}},
LogicOp: "AND",
}}, query.Filters...)
if len(query.Filters) > 0 {
query.Filters = append([]utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpEqual,
Value: "1",
}},
LogicOp: "AND",
}}, query.Filters...)
} else {
query.Filters = []utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpEqual,
Value: "1",
}},
LogicOp: "AND",
}}
}
// Execute concurrent queries
var (
items []qris.Qris
total int
wg sync.WaitGroup
errChan = make(chan error, 2)
mu sync.Mutex
)
// Execute queries sequentially to avoid race conditions
var total int
var items []qris.Qris
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
countQuery := query
countQuery.Limit = 0
countQuery.Offset = 0
countSQL, countArgs, err := builder.BuildCountQuery(countQuery)
// 1. Get total count first
countQuery := query
countQuery.Limit = 0
countQuery.Offset = 0
countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery)
if err != nil {
return nil, 0, fmt.Errorf("failed to build count query: %w", err)
}
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("failed to get total count: %w", err)
}
// 2. Get main data
mainSQL, mainArgs, err := mainBuilder.BuildQuery(query)
// Inside goroutines, before executing queries:
log.Printf("[DEBUG] Dynamic SQL Query: %s", mainSQL)
log.Printf("[DEBUG] Dynamic SQL Args: %#v", mainArgs)
if err != nil {
return nil, 0, fmt.Errorf("failed to build main query: %w", err)
}
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute main query: %w", err)
}
defer rows.Close()
for rows.Next() {
item, err := h.scanQris(rows)
if err != nil {
errChan <- fmt.Errorf("failed to build count query: %w", err)
return
return nil, 0, fmt.Errorf("failed to scan qris: %w", err)
}
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
errChan <- fmt.Errorf("failed to get total count: %w", err)
return
}
}()
items = append(items, item)
}
// Fetch main data
wg.Add(1)
go func() {
defer wg.Done()
mainSQL, mainArgs, err := builder.BuildQuery(query)
if err != nil {
errChan <- fmt.Errorf("failed to build main query: %w", err)
return
}
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
if err != nil {
errChan <- fmt.Errorf("failed to execute main query: %w", err)
return
}
defer rows.Close()
var results []qris.Qris
for rows.Next() {
item, err := h.scanQris(rows)
if err != nil {
errChan <- fmt.Errorf("failed to scan search: %w", err)
return
}
results = append(results, item)
}
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("rows iteration error: %w", err)
return
}
mu.Lock()
items = results
mu.Unlock()
}()
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return nil, 0, err
}
if err := rows.Err(); err != nil {
return nil, 0, fmt.Errorf("rows iteration error: %w", err)
}
return items, total, nil
@@ -875,7 +955,7 @@ func (h *QrisHandler) fetchDynamic(ctx context.Context, dbConn *sql.DB, query ut
// @Success 200 {object} qris.QrisGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /qris/search/dynamic [get]
// @Router /qris/dynamic [get]
func (h *QrisHandler) GetSearchDynamic(c *gin.Context) {
// Parse query parameters
parser := utils.NewQueryParser().SetLimits(10, 100)
@@ -885,6 +965,8 @@ func (h *QrisHandler) GetSearchDynamic(c *gin.Context) {
return
}
log.Printf("[DEBUG] DynamicQuery.Filters: %#v", dynamicQuery.Filters)
// Get database connection
dbConn, err := h.db.GetDB("simrs_backup")
if err != nil {
@@ -919,7 +1001,57 @@ func (h *QrisHandler) SearchAdvanced(c *gin.Context) {
// Parse complex search parameters
searchQuery := c.Query("q")
if searchQuery == "" {
h.respondError(c, "Search query is required", fmt.Errorf("empty search query"), http.StatusBadRequest)
// If no search query provided, return all records with default sorting
query := utils.DynamicQuery{
Fields: []string{"*"},
Filters: []utils.FilterGroup{}, // Empty filters - fetchDynamic will add default deleted filter
Sort: []utils.SortField{{
Column: "created_at",
Order: "DESC",
}},
Limit: 20,
Offset: 0,
}
// Parse pagination if provided
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
query.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
query.Offset = o
}
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute query to get all records
items, total, err := h.fetchDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, total)
response := qris.QrisGetResponse{
Message: "All records retrieved (no search query provided)",
Data: items,
Meta: meta,
}
c.JSON(http.StatusOK, response)
return
}
@@ -930,8 +1062,8 @@ func (h *QrisHandler) SearchAdvanced(c *gin.Context) {
Filters: []utils.DynamicFilter{
{
Column: "status",
Operator: utils.OpNotEqual,
Value: "0",
Operator: utils.OpEqual,
Value: "1",
},
{
Column: "ip",

View File

@@ -6,7 +6,7 @@ import (
"time"
)
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
// NullableInt32 - your existing implementation
type NullableInt32 struct {
Int32 int32 `json:"int32,omitempty"`
Valid bool `json:"valid"`
@@ -31,6 +31,56 @@ func (n NullableInt32) Value() (driver.Value, error) {
return n.Int32, nil
}
// NullableString provides consistent nullable string handling
type NullableString struct {
String string `json:"string,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableString
func (n *NullableString) Scan(value interface{}) error {
var ns sql.NullString
if err := ns.Scan(value); err != nil {
return err
}
n.String = ns.String
n.Valid = ns.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableString
func (n NullableString) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.String, nil
}
// NullableTime provides consistent nullable time handling
type NullableTime struct {
Time time.Time `json:"time,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableTime
func (n *NullableTime) Scan(value interface{}) error {
var nt sql.NullTime
if err := nt.Scan(value); err != nil {
return err
}
n.Time = nt.Time
n.Valid = nt.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableTime
func (n NullableTime) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Time, nil
}
// Metadata untuk pagination - dioptimalkan
type MetaResponse struct {
Limit int `json:"limit"`

View File

@@ -14,7 +14,7 @@ type Qris struct {
Status string `json:"status" db:"status"`
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
PosDevice sql.NullString `json:"posdevice,omitempty" db:"posdevice"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
CreatedAt sql.NullTime `json:"created_at,omitempty" db:"created_at"`
InvoiceNumber sql.NullString `json:"invoice_number,omitempty" db:"invoice_number"`
UpdatedAt sql.NullTime `json:"updated_at,omitempty" db:"updated_at"`
DisplayName sql.NullString `json:"display_name,omitempty" db:"display_name"`
@@ -32,6 +32,7 @@ func (r Qris) MarshalJSON() ([]byte, error) {
PosDevice *string `json:"posdevice,omitempty"`
InvoiceNumber *string `json:"invoice_number,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
DisplayName *string `json:"display_name,omitempty"`
*Alias
}{
@@ -51,6 +52,9 @@ func (r Qris) MarshalJSON() ([]byte, error) {
if r.UpdatedAt.Valid {
aux.UpdatedAt = &r.UpdatedAt.Time
}
if r.CreatedAt.Valid {
aux.CreatedAt = &r.CreatedAt.Time
}
if r.DisplayName.Valid {
aux.DisplayName = &r.DisplayName.String
}
@@ -58,12 +62,12 @@ func (r Qris) MarshalJSON() ([]byte, error) {
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *Qris) GetName() string {
/*func (r *Qris) GetName() string {
if r.DisplayName.Valid {
return r.DisplayName.String
}
return ""
}
}*/
// Response struct untuk GET by ID
type QrisGetByIDResponse struct {
@@ -74,7 +78,7 @@ type QrisGetByIDResponse struct {
// Response struct untuk GET by IP
type QrisGetByIPResponse struct {
Message string `json:"message"`
Data *Qris `json:"data"`
Data []Qris `json:"data"`
}
// Enhanced GET response dengan pagination dan aggregation
@@ -87,8 +91,11 @@ type QrisGetResponse struct {
// Filter struct untuk query parameters
type QrisFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
Status *string `json:"status,omitempty" form:"status"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
PosDevice *string `json:"posdevice,omitempty" form:"posdevice"`
DisplayName *string `json:"display_name,omitempty" form:"display_name"`
IPAddress *string `json:"ip,omitempty" form:"ip"`
}

View File

@@ -76,7 +76,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
qrisQrisGroup.GET("/ip/:ip", qrisQrisHandler.GetQrisByIP)
qrisQrisGroup.GET("/stats", qrisQrisHandler.GetQrisStats)
qrisQrisGroup.GET("/dynamic", qrisQrisHandler.GetSearchDynamic)
qrisQrisGroup.GET("/search", qrisQrisHandler.SearchAdvanced)
//qrisQrisGroup.GET("/search", qrisQrisHandler.SearchAdvanced)
}
// Search endpoints

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"reflect"
"strings"
"sync"
)
// FilterOperator represents supported filter operators
@@ -67,6 +68,7 @@ type QueryBuilder struct {
columnMapping map[string]string // Maps API field names to DB column names
allowedColumns map[string]bool // Security: only allow specified columns
paramCounter int
mu *sync.RWMutex
}
// NewQueryBuilder creates a new query builder instance
@@ -174,16 +176,23 @@ func (qb *QueryBuilder) buildSelectClause(fields []string) string {
continue
}
// Check if it's an expression (contains spaces, parentheses, etc.)
if strings.Contains(field, " ") || strings.Contains(field, "(") || strings.Contains(field, ")") {
// Expression, add as is
selectedFields = append(selectedFields, field)
continue
}
// Security check: only allow specified columns (check original field name)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[field] {
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))
}
@@ -218,7 +227,7 @@ func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup) (string, []
conditions = append(conditions, logicOp)
}
conditions = append(conditions, "("+groupCondition+")")
conditions = append(conditions, groupCondition)
args = append(args, groupArgs...)
}
}
@@ -262,34 +271,46 @@ func (qb *QueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface
// buildFilterCondition builds a single filter condition
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []interface{}, error) {
// Security check (check original field name)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[filter.Column] {
return "", nil, nil
}
// 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:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s = $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpNotEqual:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s != $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLike:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s LIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpILike:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
@@ -326,22 +347,37 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in
return fmt.Sprintf("%s NOT IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
case OpGreaterThan:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s > $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpGreaterThanEqual:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s >= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLessThan:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s < $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLessThanEqual:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
return fmt.Sprintf("%s <= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpBetween:
if filter.Value == nil {
return "", nil, nil
}
values := qb.parseArrayValue(filter.Value)
if len(values) != 2 {
return "", nil, fmt.Errorf("between operator requires exactly 2 values")
@@ -353,6 +389,9 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in
return fmt.Sprintf("%s BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
case OpNotBetween:
if filter.Value == nil {
return "", nil, nil
}
values := qb.parseArrayValue(filter.Value)
if len(values) != 2 {
return "", nil, fmt.Errorf("not between operator requires exactly 2 values")
@@ -370,21 +409,33 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
case OpContains:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
value := fmt.Sprintf("%%%v%%", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpNotContains:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
value := fmt.Sprintf("%%%v%%", filter.Value)
return fmt.Sprintf("%s NOT ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpStartsWith:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
value := fmt.Sprintf("%v%%", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpEndsWith:
if filter.Value == nil {
return "", nil, nil
}
qb.paramCounter++
value := fmt.Sprintf("%%%v", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
@@ -435,15 +486,16 @@ func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
var orderParts []string
for _, sort := range sortFields {
column := sort.Column
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check
// Security check (check original field name)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
continue
}
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
order := "ASC"
if sort.Order != "" {
order = strings.ToUpper(sort.Order)