first commit
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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
|
||||
mu *sync.RWMutex
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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:
|
||||
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")
|
||||
}
|
||||
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:
|
||||
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")
|
||||
}
|
||||
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:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user