This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user