initiate repo
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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,918 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
"yourpackage/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Inisialisasi koneksi database
|
||||
db, err := sqlx.Connect("postgres", "user=postgres dbname=testdb sslmode=disable")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Inisialisasi QueryBuilder
|
||||
qb := utils.NewQueryBuilder(utils.DBTypePostgreSQL)
|
||||
|
||||
// Contoh penggunaan
|
||||
simpleQueryExample(db, qb)
|
||||
complexQueryExample(db, qb)
|
||||
nestedJoinExample(db, qb)
|
||||
multiJoinExample(db, qb)
|
||||
commonQueriesExample(db, qb)
|
||||
jsonQueryExample(db, qb)
|
||||
windowFunctionExample(db, qb)
|
||||
cteExample(db, qb)
|
||||
unionExample(db, qb)
|
||||
aggregateExample(db, qb)
|
||||
}
|
||||
|
||||
func simpleQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Simple Query Example ===")
|
||||
|
||||
// Query sederhana dengan filter
|
||||
query := utils.DynamicQuery{
|
||||
From: "users",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id", Alias: "user_id"},
|
||||
{Expression: "name", Alias: "user_name"},
|
||||
{Expression: "email"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
{Column: "created_at", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(0, -1, 0)},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing simple query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users\n", len(results))
|
||||
for _, user := range results {
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
}
|
||||
|
||||
func complexQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Complex Query Example ===")
|
||||
|
||||
// Query dengan nested filter dan berbagai operator
|
||||
query := utils.DynamicQuery{
|
||||
From: "orders",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id", Alias: "order_id"},
|
||||
{Expression: "customer_id"},
|
||||
{Expression: "total_amount"},
|
||||
{Expression: "order_date"},
|
||||
{Expression: "status"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpIn, Value: []string{"completed", "processing"}},
|
||||
{Column: "total_amount", Operator: utils.OpGreaterThan, Value: 1000},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "order_date", Operator: utils.OpBetween, Value: []interface{}{time.Now().AddDate(0, -3, 0), time.Now()}},
|
||||
{Column: "customer_id", Operator: utils.OpNotIn, Value: []int{1, 2, 3}},
|
||||
},
|
||||
LogicOp: "OR",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "order_date", Order: "DESC"},
|
||||
{Column: "total_amount", Order: "DESC"},
|
||||
},
|
||||
Limit: 20,
|
||||
Offset: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing complex query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d orders\n", len(results))
|
||||
for _, order := range results {
|
||||
fmt.Printf("Order: %+v\n", order)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedJoinExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Nested Join Example ===")
|
||||
|
||||
// Query dengan nested join
|
||||
query := utils.DynamicQuery{
|
||||
From: "customers",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "customers.id", Alias: "customer_id"},
|
||||
{Expression: "customers.name", Alias: "customer_name"},
|
||||
{Expression: "orders.id", Alias: "order_id"},
|
||||
{Expression: "orders.total_amount"},
|
||||
{Expression: "order_items.product_id"},
|
||||
{Expression: "order_items.quantity"},
|
||||
{Expression: "products.name", Alias: "product_name"},
|
||||
},
|
||||
Joins: []utils.Join{
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "orders",
|
||||
Alias: "orders",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "customers.id", Operator: utils.OpEqual, Value: "orders.customer_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "order_items",
|
||||
Alias: "order_items",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "orders.id", Operator: utils.OpEqual, Value: "order_items.order_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "products",
|
||||
Alias: "products",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "order_items.product_id", Operator: utils.OpEqual, Value: "products.id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "customers.status", Operator: utils.OpEqual, Value: "active"},
|
||||
{Column: "orders.status", Operator: utils.OpEqual, Value: "completed"},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "customers.name", Order: "ASC"},
|
||||
{Column: "orders.id", Order: "DESC"},
|
||||
},
|
||||
Limit: 50,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing nested join query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d customer-order-product records\n", len(results))
|
||||
for _, record := range results {
|
||||
fmt.Printf("Record: %+v\n", record)
|
||||
}
|
||||
}
|
||||
|
||||
func multiJoinExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Multi Join Example ===")
|
||||
|
||||
// Query dengan multiple join types
|
||||
query := utils.DynamicQuery{
|
||||
From: "employees",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "employees.id", Alias: "employee_id"},
|
||||
{Expression: "employees.name", Alias: "employee_name"},
|
||||
{Expression: "departments.name", Alias: "department_name"},
|
||||
{Expression: "projects.name", Alias: "project_name"},
|
||||
{Expression: "tasks.title", Alias: "task_title"},
|
||||
{Expression: "task_assignments.assigned_date"},
|
||||
},
|
||||
Joins: []utils.Join{
|
||||
{
|
||||
Type: "INNER",
|
||||
Table: "departments",
|
||||
Alias: "departments",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "employees.department_id", Operator: utils.OpEqual, Value: "departments.id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "task_assignments",
|
||||
Alias: "task_assignments",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "employees.id", Operator: utils.OpEqual, Value: "task_assignments.employee_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "tasks",
|
||||
Alias: "tasks",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "task_assignments.task_id", Operator: utils.OpEqual, Value: "tasks.id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "projects",
|
||||
Alias: "projects",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "tasks.project_id", Operator: utils.OpEqual, Value: "projects.id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "employees.status", Operator: utils.OpEqual, Value: "active"},
|
||||
{Column: "departments.status", Operator: utils.OpEqual, Value: "active"},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "departments.name", Order: "ASC"},
|
||||
{Column: "employees.name", Order: "ASC"},
|
||||
{Column: "task_assignments.assigned_date", Order: "DESC"},
|
||||
},
|
||||
Limit: 100,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing multi join query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d employee-task-project records\n", len(results))
|
||||
for _, record := range results {
|
||||
fmt.Printf("Record: %+v\n", record)
|
||||
}
|
||||
}
|
||||
|
||||
func commonQueriesExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Common Queries Example ===")
|
||||
|
||||
// 1. Query dengan LIKE/ILIKE
|
||||
likeQuery := utils.DynamicQuery{
|
||||
From: "products",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
{Expression: "price"},
|
||||
{Expression: "category"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "name", Operator: utils.OpILike, Value: "%laptop%"},
|
||||
{Column: "category", Operator: utils.OpEqual, Value: "electronics"},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "price", Order: "ASC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var products []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, likeQuery, &products)
|
||||
if err != nil {
|
||||
log.Printf("Error executing LIKE query: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Found %d products matching 'laptop'\n", len(products))
|
||||
}
|
||||
|
||||
// 2. Query dengan pagination
|
||||
page := 2
|
||||
pageSize := 20
|
||||
paginationQuery := utils.DynamicQuery{
|
||||
From: "orders",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "customer_id"},
|
||||
{Expression: "total_amount"},
|
||||
{Expression: "order_date"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "completed"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "order_date", Order: "DESC"},
|
||||
},
|
||||
Limit: pageSize,
|
||||
Offset: (page - 1) * pageSize,
|
||||
}
|
||||
|
||||
var orders []map[string]interface{}
|
||||
err = qb.ExecuteQuery(context.Background(), db, paginationQuery, &orders)
|
||||
if err != nil {
|
||||
log.Printf("Error executing pagination query: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Found %d orders on page %d\n", len(orders), page)
|
||||
}
|
||||
|
||||
// 3. Query dengan NULL/NOT NULL
|
||||
nullQuery := utils.DynamicQuery{
|
||||
From: "customers",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
{Expression: "email"},
|
||||
{Expression: "phone"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "email", Operator: utils.OpNotNull},
|
||||
{Column: "phone", Operator: utils.OpNull},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var customers []map[string]interface{}
|
||||
err = qb.ExecuteQuery(context.Background(), db, nullQuery, &customers)
|
||||
if err != nil {
|
||||
log.Printf("Error executing NULL query: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Found %d customers with email but no phone\n", len(customers))
|
||||
}
|
||||
|
||||
// 4. Query dengan BETWEEN
|
||||
betweenQuery := utils.DynamicQuery{
|
||||
From: "transactions",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "account_id"},
|
||||
{Expression: "amount"},
|
||||
{Expression: "transaction_date"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "amount", Operator: utils.OpBetween, Value: []interface{}{100, 1000}},
|
||||
{Column: "transaction_date", Operator: utils.OpBetween, Value: []interface{}{time.Now().AddDate(0, -1, 0), time.Now()}},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "transaction_date", Order: "DESC"},
|
||||
},
|
||||
Limit: 20,
|
||||
}
|
||||
|
||||
var transactions []map[string]interface{}
|
||||
err = qb.ExecuteQuery(context.Background(), db, betweenQuery, &transactions)
|
||||
if err != nil {
|
||||
log.Printf("Error executing BETWEEN query: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Found %d transactions between $100 and $1000 in the last month\n", len(transactions))
|
||||
}
|
||||
}
|
||||
|
||||
func jsonQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== JSON Query Example ===")
|
||||
|
||||
// Query dengan operasi JSON
|
||||
query := utils.DynamicQuery{
|
||||
From: "products",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
{Expression: "price"},
|
||||
{Expression: "attributes"},
|
||||
},
|
||||
JsonOperations: []utils.JsonOperation{
|
||||
{
|
||||
Type: "extract",
|
||||
Column: "attributes",
|
||||
Path: "$.color",
|
||||
Alias: "color",
|
||||
},
|
||||
{
|
||||
Type: "extract",
|
||||
Column: "attributes",
|
||||
Path: "$.size",
|
||||
Alias: "size",
|
||||
},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{
|
||||
Column: "attributes",
|
||||
Operator: utils.OpJsonContains,
|
||||
Value: map[string]interface{}{"category": "electronics"},
|
||||
Options: map[string]interface{}{"path": "$"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing JSON query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d products with JSON attributes\n", len(results))
|
||||
for _, product := range results {
|
||||
fmt.Printf("Product: %+v\n", product)
|
||||
}
|
||||
}
|
||||
|
||||
func windowFunctionExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Window Function Example ===")
|
||||
|
||||
// Query dengan window functions
|
||||
query := utils.DynamicQuery{
|
||||
From: "sales",
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "salesperson_id"},
|
||||
{Expression: "amount"},
|
||||
{Expression: "sale_date"},
|
||||
},
|
||||
WindowFunctions: []utils.WindowFunction{
|
||||
{
|
||||
Function: "ROW_NUMBER",
|
||||
Over: "salesperson_id",
|
||||
OrderBy: "amount DESC",
|
||||
Alias: "sales_rank",
|
||||
},
|
||||
{
|
||||
Function: "SUM",
|
||||
Over: "salesperson_id",
|
||||
OrderBy: "sale_date",
|
||||
Frame: "ROWS UNBOUNDED PRECEDING",
|
||||
Alias: "running_total",
|
||||
},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "sale_date", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(0, -6, 0)},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "salesperson_id", Order: "ASC"},
|
||||
{Column: "amount", Order: "DESC"},
|
||||
},
|
||||
Limit: 50,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing window function query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d sales records with window functions\n", len(results))
|
||||
for _, sale := range results {
|
||||
fmt.Printf("Sale: %+v\n", sale)
|
||||
}
|
||||
}
|
||||
|
||||
func cteExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== CTE Example ===")
|
||||
|
||||
// Query dengan CTE
|
||||
query := utils.DynamicQuery{
|
||||
CTEs: []utils.CTE{
|
||||
{
|
||||
Name: "monthly_sales",
|
||||
Query: utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "salesperson_id"},
|
||||
{Expression: "EXTRACT(MONTH FROM sale_date) AS month"},
|
||||
{Expression: "SUM(amount) AS total"},
|
||||
},
|
||||
From: "sales",
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "sale_date", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(-1, 0, 0)},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []string{"salesperson_id", "EXTRACT(MONTH FROM sale_date)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "top_salespeople",
|
||||
Query: utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "salesperson_id"},
|
||||
{Expression: "SUM(total) AS yearly_total"},
|
||||
},
|
||||
From: "monthly_sales",
|
||||
GroupBy: []string{"salesperson_id"},
|
||||
Having: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "SUM(total)", Operator: utils.OpGreaterThan, Value: 10000},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "salespeople.id"},
|
||||
{Expression: "salespeople.name"},
|
||||
{Expression: "top_salespeople.yearly_total"},
|
||||
},
|
||||
From: "salespeople",
|
||||
Joins: []utils.Join{
|
||||
{
|
||||
Type: "INNER",
|
||||
Table: "top_salespeople",
|
||||
Alias: "top_salespeople",
|
||||
OnConditions: utils.FilterGroup{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "salespeople.id", Operator: utils.OpEqual, Value: "top_salespeople.salesperson_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "top_salespeople.yearly_total", Order: "DESC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing CTE query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d top salespeople\n", len(results))
|
||||
for _, salesperson := range results {
|
||||
fmt.Printf("Salesperson: %+v\n", salesperson)
|
||||
}
|
||||
}
|
||||
|
||||
func unionExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== UNION Example ===")
|
||||
|
||||
// Query dengan UNION
|
||||
query := utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
{Expression: "email"},
|
||||
{Expression: "'customer' AS user_type"},
|
||||
},
|
||||
From: "customers",
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Unions: []utils.Union{
|
||||
{
|
||||
Type: "UNION ALL",
|
||||
Query: utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
{Expression: "email"},
|
||||
{Expression: "'employee' AS user_type"},
|
||||
},
|
||||
From: "employees",
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
},
|
||||
Limit: 20,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing UNION query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users (customers + employees)\n", len(results))
|
||||
for _, user := range results {
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
}
|
||||
|
||||
func aggregateExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Aggregate Example ===")
|
||||
|
||||
// Query dengan fungsi agregasi
|
||||
query := utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "category"},
|
||||
{Expression: "COUNT(*) AS product_count"},
|
||||
{Expression: "AVG(price) AS avg_price"},
|
||||
{Expression: "MIN(price) AS min_price"},
|
||||
{Expression: "MAX(price) AS max_price"},
|
||||
{Expression: "SUM(stock_quantity) AS total_stock"},
|
||||
},
|
||||
From: "products",
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []string{"category"},
|
||||
Having: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "COUNT(*)", Operator: utils.OpGreaterThan, Value: 5},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "product_count", Order: "DESC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing aggregate query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d product categories\n", len(results))
|
||||
for _, category := range results {
|
||||
fmt.Printf("Category: %+v\n", category)
|
||||
}
|
||||
}
|
||||
|
||||
func crudOperationsExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== CRUD Operations Example ===")
|
||||
|
||||
// INSERT
|
||||
insertData := utils.InsertData{
|
||||
Columns: []string{"name", "email", "status", "created_at"},
|
||||
Values: []interface{}{"John Doe", "john@example.com", "active", time.Now()},
|
||||
JsonValues: map[string]interface{}{
|
||||
"preferences": map[string]interface{}{
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := qb.ExecuteInsert(context.Background(), db, "customers", insertData, "id")
|
||||
if err != nil {
|
||||
log.Printf("Error executing INSERT: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
log.Printf("Error getting inserted ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Inserted customer with ID: %d\n", id)
|
||||
|
||||
// UPDATE
|
||||
updateData := utils.UpdateData{
|
||||
Columns: []string{"name", "status"},
|
||||
Values: []interface{}{"John Smith", "inactive"},
|
||||
JsonUpdates: map[string]utils.JsonUpdate{
|
||||
"preferences": {
|
||||
Path: "$.theme",
|
||||
Value: "light",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
filters := []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "id", Operator: utils.OpEqual, Value: id},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err = qb.ExecuteUpdate(context.Background(), db, "customers", updateData, filters, "updated_at")
|
||||
if err != nil {
|
||||
log.Printf("Error executing UPDATE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Printf("Error getting rows affected: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Updated %d customer(s)\n", rowsAffected)
|
||||
|
||||
// DELETE
|
||||
result, err = qb.ExecuteDelete(context.Background(), db, "customers", filters)
|
||||
if err != nil {
|
||||
log.Printf("Error executing DELETE: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err = result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Printf("Error getting rows affected: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted %d customer(s)\n", rowsAffected)
|
||||
}
|
||||
|
||||
|
||||
func mongoExample() {
|
||||
fmt.Println("\n=== MongoDB Example ===")
|
||||
|
||||
// Inisialisasi koneksi MongoDB
|
||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://localhost:27017"))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to MongoDB: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
db := client.Database("testdb")
|
||||
collection := db.Collection("users")
|
||||
|
||||
// Inisialisasi MongoQueryBuilder
|
||||
mqb := utils.NewMongoQueryBuilder()
|
||||
|
||||
// Query sederhana
|
||||
query := utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "name"},
|
||||
{Expression: "email"},
|
||||
{Expression: "status"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
{Column: "age", Operator: utils.OpGreaterThan, Value: 18},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
},
|
||||
},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
err = mqb.ExecuteFind(context.Background(), collection, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing MongoDB query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users\n", len(results))
|
||||
for _, user := range results {
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
|
||||
// Aggregation pipeline
|
||||
aggQuery := utils.DynamicQuery{
|
||||
Fields: []utils.SelectField{
|
||||
{Expression: "department", Alias: "_id"},
|
||||
{Expression: "COUNT(*)", Alias: "employee_count"},
|
||||
{Expression: "AVG(salary)", Alias: "avg_salary"},
|
||||
},
|
||||
Filters: []utils.FilterGroup{
|
||||
{
|
||||
Filters: []utils.DynamicFilter{
|
||||
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []string{"department"},
|
||||
Sort: []utils.SortField{
|
||||
{Column: "employee_count", Order: "DESC"},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var aggResults []map[string]interface{}
|
||||
err = mqb.ExecuteAggregate(context.Background(), collection, aggQuery, &aggResults)
|
||||
if err != nil {
|
||||
log.Printf("Error executing MongoDB aggregation: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d departments\n", len(aggResults))
|
||||
for _, dept := range aggResults {
|
||||
fmt.Printf("Department: %+v\n", dept)
|
||||
}
|
||||
}
|
||||
|
||||
func queryParserExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||
fmt.Println("\n=== Query Parser Example ===")
|
||||
|
||||
// Inisialisasi QueryParser
|
||||
qp := utils.NewQueryParser()
|
||||
|
||||
// Parse URL query parameters
|
||||
values := url.Values{}
|
||||
values.Add("fields", "id,name,email,status")
|
||||
values.Add("filter[status][_eq]", "active")
|
||||
values.Add("filter[created_at][_gte]", "2023-01-01")
|
||||
values.Add("filter[age][_between]", "18,65")
|
||||
values.Add("sort", "+name,-created_at")
|
||||
values.Add("limit", "20")
|
||||
values.Add("offset", "10")
|
||||
|
||||
// Parse query parameters into DynamicQuery
|
||||
query, err := qp.ParseQuery(values, "users")
|
||||
if err != nil {
|
||||
log.Printf("Error parsing query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the parsed query
|
||||
var results []map[string]interface{}
|
||||
err = qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error executing parsed query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users using parsed query\n", len(results))
|
||||
for _, user := range results {
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,943 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
"api-service/internal/utils/query"
|
||||
"api-service/internal/validation"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// This file provides comprehensive examples of using the query builder library
|
||||
// for performing various database operations including CRUD, transactions, joins, etc.
|
||||
// Each example function demonstrates how to build queries, print them, and execute them.
|
||||
// =============================================================================
|
||||
// DEFINISI MODEL (CONTOH)
|
||||
// =============================================================================
|
||||
|
||||
// User adalah contoh struct untuk tabel 'users'.
|
||||
type User struct {
|
||||
ID int `db:"id" bson:"_id,omitempty"`
|
||||
Name string `db:"name" bson:"name"`
|
||||
Email string `db:"email" bson:"email"`
|
||||
Status string `db:"status" bson:"status"`
|
||||
CreatedAt time.Time `db:"created_at" bson:"created_at"`
|
||||
}
|
||||
|
||||
// Post adalah contoh struct untuk tabel 'posts'.
|
||||
type Post struct {
|
||||
ID int `db:"id" bson:"_id,omitempty"`
|
||||
UserID int `db:"user_id" bson:"user_id"`
|
||||
Title string `db:"title" bson:"title"`
|
||||
Content string `db:"content" bson:"content"`
|
||||
CreatedAt time.Time `db:"created_at" bson:"created_at"`
|
||||
}
|
||||
|
||||
// Employee adalah contoh struct untuk tabel 'employees' dengan kolom JSON.
|
||||
type Employee struct {
|
||||
ID int `db:"id" bson:"_id,omitempty"`
|
||||
Name string `db:"name" bson:"name"`
|
||||
Department string `db:"department" bson:"department"`
|
||||
Salary float64 `db:"salary" bson:"salary"`
|
||||
Metadata map[string]interface{} `db:"metadata" bson:"metadata"` // Kolom JSON/JSONB
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FUNGSI UTAMA
|
||||
// =============================================================================
|
||||
|
||||
func main() {
|
||||
cfg := setupConfig()
|
||||
dbService := database.New(cfg)
|
||||
|
||||
fmt.Println("============================================================")
|
||||
fmt.Println(" CONTOH 1: QUERY DASAR (SELECT, INSERT, UPDATE, DELETE)")
|
||||
fmt.Println("============================================================")
|
||||
basicCRUDExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 2: TRANSAKSI SQL (POSTGRESQL)")
|
||||
fmt.Println("============================================================")
|
||||
sqlTransactionExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 3: TRANSAKSI MONGODB")
|
||||
fmt.Println("============================================================")
|
||||
mongoTransactionExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 4: QUERY DENGAN FILTER DAN PAGINASI")
|
||||
fmt.Println("============================================================")
|
||||
filterAndPaginationExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 5: QUERY DENGAN JOIN")
|
||||
fmt.Println("============================================================")
|
||||
joinExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 6: QUERY DENGAN CTE (COMMON TABLE EXPRESSION)")
|
||||
fmt.Println("============================================================")
|
||||
cteExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 7: QUERY DENGAN WINDOW FUNCTION")
|
||||
fmt.Println("============================================================")
|
||||
windowFunctionExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 8: VALIDASI DATA DINAMIS")
|
||||
fmt.Println("============================================================")
|
||||
validationExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 9: OPERASI JSON")
|
||||
fmt.Println("============================================================")
|
||||
jsonQueryExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 10: QUERY MONGODB (CRUD & AGGREGATION)")
|
||||
fmt.Println("============================================================")
|
||||
mongodbExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 11: PENGGUNAAN READ REPLICA")
|
||||
fmt.Println("============================================================")
|
||||
readReplicaExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 12: HEALTH CHECK DATABASE")
|
||||
fmt.Println("============================================================")
|
||||
healthCheckExample(dbService)
|
||||
|
||||
fmt.Println("\n============================================================")
|
||||
fmt.Println(" CONTOH 13: PARSING QUERY DARI URL")
|
||||
fmt.Println("============================================================")
|
||||
urlQueryParsingExample(dbService)
|
||||
}
|
||||
|
||||
func setupConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Databases: map[string]config.DatabaseConfig{
|
||||
"main": {
|
||||
Type: "postgres",
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Database: "company_db",
|
||||
SSLMode: "disable",
|
||||
MaxOpenConns: 25,
|
||||
MaxIdleConns: 5,
|
||||
ConnMaxLifetime: time.Hour,
|
||||
},
|
||||
},
|
||||
"mongodb": config.DatabaseConfig{
|
||||
Type: "mongodb",
|
||||
Host: "localhost",
|
||||
Port: 27017,
|
||||
Database: "company_db",
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 1: QUERY DASAR (CRUD)
|
||||
// =============================================================================
|
||||
|
||||
// basicCRUDExample demonstrates basic Create, Read, Update, Delete operations using the query builder.
|
||||
// It shows how to build SQL queries, print them, and execute them while displaying results.
|
||||
// Expected output: Prints INSERT SQL and result (new ID), SELECT SQL and user data, UPDATE SQL and affected rows, DELETE SQL and affected rows.
|
||||
// Example raw queries:
|
||||
// INSERT: INSERT INTO users (name, email, status) VALUES ($1, $2, $3) RETURNING id
|
||||
// SELECT: SELECT * FROM users WHERE id = $1
|
||||
// UPDATE: UPDATE users SET status = $1 WHERE id = $2
|
||||
// DELETE: DELETE FROM users WHERE id = $1
|
||||
func basicCRUDExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
// --- INSERT ---
|
||||
fmt.Println("\n--- Operasi INSERT ---")
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"name", "email", "status"},
|
||||
Values: []interface{}{"Alice", "alice@example.com", "active"},
|
||||
}
|
||||
sql, args, err := qb.BuildInsertQuery("users", insertData, "id")
|
||||
if err != nil {
|
||||
log.Printf("Error building INSERT: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated INSERT SQL: %s\nArgs: %v\n", sql, args)
|
||||
result, err := qb.ExecuteInsert(ctx, db, "users", insertData, "id")
|
||||
if err != nil {
|
||||
log.Printf("Error INSERT: %v", err)
|
||||
return
|
||||
}
|
||||
newID, _ := result.LastInsertId()
|
||||
fmt.Printf("-> INSERT: Berhasil menambah user dengan ID: %d\n", newID)
|
||||
|
||||
// --- SELECT (Single Row) ---
|
||||
fmt.Println("\n--- Operasi SELECT ---")
|
||||
var user User
|
||||
selectQuery := query.DynamicQuery{
|
||||
Fields: []query.SelectField{{Expression: "*"}},
|
||||
From: "users",
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||
}},
|
||||
}
|
||||
sql, args, err = qb.BuildQuery(selectQuery)
|
||||
if err != nil {
|
||||
log.Printf("Error building SELECT: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQueryRow(ctx, db, selectQuery, &user)
|
||||
if err != nil {
|
||||
log.Printf("Error SELECT single row: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> SELECT (Single Row): Berhasil mengambil user: %+v\n", user)
|
||||
|
||||
// --- UPDATE ---
|
||||
fmt.Println("\n--- Operasi UPDATE ---")
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"status"},
|
||||
Values: []interface{}{"inactive"},
|
||||
}
|
||||
updateFilter := []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||
}}
|
||||
sql, args, err = qb.BuildUpdateQuery("users", updateData, updateFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error building UPDATE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated UPDATE SQL: %s\nArgs: %v\n", sql, args)
|
||||
_, err = qb.ExecuteUpdate(ctx, db, "users", updateData, updateFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error UPDATE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> UPDATE: Berhasil memperbarui status user dengan ID: %d\n", newID)
|
||||
|
||||
// --- DELETE ---
|
||||
fmt.Println("\n--- Operasi DELETE ---")
|
||||
deleteFilter := []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||
}}
|
||||
sql, args, err = qb.BuildDeleteQuery("users", deleteFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error building DELETE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated DELETE SQL: %s\nArgs: %v\n", sql, args)
|
||||
_, err = qb.ExecuteDelete(ctx, db, "users", deleteFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error DELETE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> DELETE: Berhasil menghapus user dengan ID: %d\n", newID)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 2: TRANSAKSI SQL (POSTGRESQL)
|
||||
// =============================================================================
|
||||
|
||||
// sqlTransactionExample demonstrates how to perform atomic transactions involving updates
|
||||
// across multiple tables using the Query Builder. It builds and prints SQL queries before execution.
|
||||
// Expected output: Prints UPDATE SQL for salaries and employees, transaction commit/rollback status, and validation results.
|
||||
// Example raw queries:
|
||||
// UPDATE salaries: UPDATE salaries SET salary = $1 WHERE employee_id = $2
|
||||
// UPDATE employees: UPDATE employees SET last_name = $1 WHERE employee_id = $2
|
||||
func sqlTransactionExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
employeeID := 123
|
||||
newSalary := 75000
|
||||
newLastName := "Doe"
|
||||
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal mendapatkan koneksi database SQL: %v", err)
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
tx, err := db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal memulai transaksi SQL: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
fmt.Println("Terjadi panic, melakukan rollback transaksi...")
|
||||
_ = tx.Rollback()
|
||||
panic(p)
|
||||
} else if err != nil {
|
||||
fmt.Printf("Transaksi dibatalkan (ROLLBACK) karena error: %v\n", err)
|
||||
_ = tx.Rollback()
|
||||
} else {
|
||||
fmt.Println("Tidak ada error, melakukan COMMIT transaksi...")
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("Gagal melakukan COMMIT transaksi: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("Memulai transaksi untuk employee_id: %d\n", employeeID)
|
||||
|
||||
// --- Operasi 1: Update gaji di tabel 'salaries' ---
|
||||
fmt.Println("\n--- Operasi 1: UPDATE salaries ---")
|
||||
salariesUpdateData := query.UpdateData{
|
||||
Columns: []string{"salary"},
|
||||
Values: []interface{}{newSalary},
|
||||
}
|
||||
salariesFilter := []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "employee_id", Operator: query.OpEqual, Value: employeeID},
|
||||
},
|
||||
},
|
||||
}
|
||||
sql, args, err := qb.BuildUpdateQuery("salaries", salariesUpdateData, salariesFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error building UPDATE salaries: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated UPDATE salaries SQL: %s\nArgs: %v\n", sql, args)
|
||||
salariesResult, err := qb.ExecuteUpdate(ctx, tx, "salaries", salariesUpdateData, salariesFilter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
salariesRowsAffected, _ := salariesResult.RowsAffected()
|
||||
fmt.Printf("-> UPDATE salaries: %d baris terpengaruh.\n", salariesRowsAffected)
|
||||
|
||||
// --- Operasi 2: Update informasi di tabel 'employees' ---
|
||||
fmt.Println("\n--- Operasi 2: UPDATE employees ---")
|
||||
employeesUpdateData := query.UpdateData{
|
||||
Columns: []string{"last_name"},
|
||||
Values: []interface{}{newLastName},
|
||||
}
|
||||
employeesFilter := []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "employee_id", Operator: query.OpEqual, Value: employeeID},
|
||||
},
|
||||
},
|
||||
}
|
||||
sql, args, err = qb.BuildUpdateQuery("employees", employeesUpdateData, employeesFilter)
|
||||
if err != nil {
|
||||
log.Printf("Error building UPDATE employees: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated UPDATE employees SQL: %s\nArgs: %v\n", sql, args)
|
||||
employeesResult, err := qb.ExecuteUpdate(ctx, tx, "employees", employeesUpdateData, employeesFilter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
employeesRowsAffected, _ := employeesResult.RowsAffected()
|
||||
fmt.Printf("-> UPDATE employees: %d baris terpengaruh.\n", employeesRowsAffected)
|
||||
|
||||
// --- Validasi Akhir Transaksi ---
|
||||
if salariesRowsAffected == 1 && employeesRowsAffected == 1 {
|
||||
fmt.Println("-> Validasi BERHASIL: Kedua tabel berhasil diperbarui.")
|
||||
} else {
|
||||
err = fmt.Errorf("validasi GAGAL: diharapkan 1 baris terupdate di setiap tabel, tetapi mendapat %d (salaries) dan %d (employees)", salariesRowsAffected, employeesRowsAffected)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 3: TRANSAKSI MONGODB
|
||||
// =============================================================================
|
||||
|
||||
// mongoTransactionExample demonstrates MongoDB transactions using the query builder.
|
||||
// It prints the filters and update operations before executing them in a transaction.
|
||||
// Expected output: Prints MongoDB filters and update operations for salaries and employees, transaction commit/abort status, and validation results.
|
||||
// Example raw queries:
|
||||
// MongoDB filters: {"employee_id": 123}
|
||||
// MongoDB updates: {"$set": {"salary": 75000}}, {"$set": {"last_name": "Doe"}}
|
||||
func mongoTransactionExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
employeeID := 123
|
||||
newSalary := 75000
|
||||
newLastName := "Doe"
|
||||
|
||||
client, err := dbService.GetMongoClient("mongodb")
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal mendapatkan klien MongoDB: %v", err)
|
||||
}
|
||||
|
||||
salariesCollection := client.Database("company_db").Collection("salaries")
|
||||
employeesCollection := client.Database("company_db").Collection("employees")
|
||||
|
||||
session, err := client.StartSession()
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal memulai sesi MongoDB: %v", err)
|
||||
}
|
||||
defer session.EndSession(ctx)
|
||||
|
||||
fmt.Printf("Memulai transaksi MongoDB untuk employee_id: %d\n", employeeID)
|
||||
|
||||
_, err = session.WithTransaction(ctx, func(sessCtx mongo.SessionContext) (interface{}, error) {
|
||||
// --- Operasi 1: Update gaji di koleksi 'salaries' ---
|
||||
fmt.Println("\n--- Operasi 1: UPDATE salaries ---")
|
||||
salariesFilter := bson.M{"employee_id": employeeID}
|
||||
salariesUpdate := bson.M{"$set": bson.M{"salary": newSalary}}
|
||||
fmt.Printf("-> MongoDB Update Salaries Filter: %#v\n", salariesFilter)
|
||||
fmt.Printf("-> MongoDB Update Salaries Operation: %#v\n", salariesUpdate)
|
||||
|
||||
salariesResult, err := salariesCollection.UpdateOne(sessCtx, salariesFilter, salariesUpdate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal update koleksi salaries: %w", err)
|
||||
}
|
||||
fmt.Printf("-> UPDATE salaries: %d dokumen cocok (matched).\n", salariesResult.MatchedCount)
|
||||
|
||||
// --- Operasi 2: Update informasi di koleksi 'employees' ---
|
||||
fmt.Println("\n--- Operasi 2: UPDATE employees ---")
|
||||
employeesFilter := bson.M{"employee_id": employeeID}
|
||||
employeesUpdate := bson.M{"$set": bson.M{"last_name": newLastName}}
|
||||
fmt.Printf("-> MongoDB Update Employees Filter: %#v\n", employeesFilter)
|
||||
fmt.Printf("-> MongoDB Update Employees Operation: %#v\n", employeesUpdate)
|
||||
|
||||
employeesResult, err := employeesCollection.UpdateOne(sessCtx, employeesFilter, employeesUpdate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal update koleksi employees: %w", err)
|
||||
}
|
||||
fmt.Printf("-> UPDATE employees: %d dokumen cocok (matched).\n", employeesResult.MatchedCount)
|
||||
|
||||
// --- Validasi Akhir Transaksi ---
|
||||
if salariesResult.MatchedCount == 1 && employeesResult.MatchedCount == 1 {
|
||||
fmt.Println("-> Validasi BERHASIL: Kedua koleksi berhasil diperbarui.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("validasi GAGAL: diharapkan 1 dokumen terupdate di setiap koleksi, tetapi mendapat %d (salaries) dan %d (employees)", salariesResult.MatchedCount, employeesResult.MatchedCount)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Transaksi MongoDB dibatalkan (ABORT) karena error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Transaksi MongoDB berhasil di-commit.")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 4: FILTER DAN PAGINASI
|
||||
// =============================================================================
|
||||
|
||||
// filterAndPaginationExample demonstrates querying with filters and pagination.
|
||||
// It builds and prints the SELECT query before executing it.
|
||||
// Expected output: Prints SELECT SQL with filters and pagination, and the number of active users found.
|
||||
// Example raw query:
|
||||
// SELECT id, name FROM users WHERE (status = $1 AND created_at > $2) ORDER BY name ASC LIMIT 5 OFFSET 10
|
||||
func filterAndPaginationExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
query := query.DynamicQuery{
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "id"},
|
||||
{Expression: "name"},
|
||||
},
|
||||
From: "users",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
LogicOp: "AND",
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "status", Operator: query.OpEqual, Value: "active"},
|
||||
{Column: "created_at", Operator: query.OpGreaterThan, Value: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{{Column: "name", Order: "ASC"}},
|
||||
Limit: 5,
|
||||
Offset: 10,
|
||||
}
|
||||
|
||||
var users []User
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
log.Printf("Error building SELECT: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQuery(ctx, db, query, &users)
|
||||
if err != nil {
|
||||
log.Printf("Error query dengan filter: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> Filter & Paginasi: Ditemukan %d user aktif (halaman 3).\n", len(users))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 5: QUERY DENGAN JOIN
|
||||
// =============================================================================
|
||||
|
||||
// joinExample demonstrates querying with JOIN operations.
|
||||
// It builds and prints the JOIN query before executing it.
|
||||
// Expected output: Prints JOIN SQL query and the number of posts with author names found.
|
||||
// Example raw query:
|
||||
// SELECT p.id AS post_id, p.title, u.name AS author_name FROM posts p INNER JOIN users u ON p.user_id = u.id LIMIT 10
|
||||
func joinExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
query := query.DynamicQuery{
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "p.id", Alias: "post_id"},
|
||||
{Expression: "p.title"},
|
||||
{Expression: "u.name", Alias: "author_name"},
|
||||
},
|
||||
From: "posts",
|
||||
Aliases: "p",
|
||||
Joins: []query.Join{
|
||||
{
|
||||
Type: "INNER",
|
||||
Table: "users",
|
||||
Alias: "u",
|
||||
OnConditions: query.FilterGroup{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "p.user_id", Operator: query.OpEqual, Value: "u.id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
PostID int `db:"post_id"`
|
||||
Title string `db:"title"`
|
||||
AuthorName string `db:"author_name"`
|
||||
}
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
log.Printf("Error building JOIN: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated JOIN SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error query JOIN: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> JOIN: Ditemukan %d post dengan nama penulis.\n", len(results))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 6: QUERY DENGAN CTE
|
||||
// =============================================================================
|
||||
|
||||
// cteExample demonstrates querying with Common Table Expressions (CTE).
|
||||
// It builds and prints the CTE query before executing it.
|
||||
// Expected output: Prints CTE SQL query and the number of users with more than 5 posts.
|
||||
func cteExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
query := query.DynamicQuery{
|
||||
CTEs: []query.CTE{
|
||||
{
|
||||
Name: "user_post_counts",
|
||||
Query: query.DynamicQuery{
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "user_id"},
|
||||
{Expression: "COUNT(*)", Alias: "post_count"},
|
||||
},
|
||||
From: "posts",
|
||||
GroupBy: []string{"user_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "u.name"},
|
||||
{Expression: "upc.post_count"},
|
||||
},
|
||||
From: "users u",
|
||||
Joins: []query.Join{
|
||||
{
|
||||
Type: "INNER",
|
||||
Table: "user_post_counts",
|
||||
Alias: "upc",
|
||||
OnConditions: query.FilterGroup{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "u.id", Operator: query.OpEqual, Value: "upc.user_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "upc.post_count", Operator: query.OpGreaterThan, Value: 5},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
Name string `db:"name"`
|
||||
PostCount int `db:"post_count"`
|
||||
}
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
log.Printf("Error building CTE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated CTE SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error query CTE: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> CTE: Ditemukan %d user dengan lebih dari 5 post.\n", len(results))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 7: WINDOW FUNCTION
|
||||
// =============================================================================
|
||||
|
||||
// windowFunctionExample demonstrates querying with window functions.
|
||||
// It builds and prints the window function query before executing it.
|
||||
// Expected output: Prints window function SQL query and the number of employees with salary rankings.
|
||||
func windowFunctionExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
query := query.DynamicQuery{
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "name"},
|
||||
{Expression: "department"},
|
||||
{Expression: "salary"},
|
||||
},
|
||||
From: "employees",
|
||||
WindowFunctions: []query.WindowFunction{
|
||||
{
|
||||
Function: "RANK",
|
||||
Over: "department",
|
||||
OrderBy: "salary DESC",
|
||||
Alias: "salary_rank",
|
||||
},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
{Column: "department", Operator: query.OpEqual, Value: "Engineering"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
Name string `db:"name"`
|
||||
Department string `db:"department"`
|
||||
Salary float64 `db:"salary"`
|
||||
SalaryRank int `db:"salary_rank"`
|
||||
}
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
log.Printf("Error building Window Function: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated Window Function SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||
if err != nil {
|
||||
log.Printf("Error query Window Function: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> Window Function: Ditemukan %d employee di departemen Engineering dengan peringkat gaji.\n", len(results))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 8: VALIDASI DATA DINAMIS
|
||||
// =============================================================================
|
||||
|
||||
// validationExample demonstrates dynamic data validation using the query builder.
|
||||
// It builds and prints the validation query before executing it.
|
||||
// Expected output: Prints validation SQL query and whether the email is duplicate or available.
|
||||
func validationExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
validator := validation.NewDynamicValidator(qb)
|
||||
|
||||
userData := map[string]interface{}{"email": "test@example.com"}
|
||||
emailRule := validation.NewUniqueFieldRule("users", "email")
|
||||
|
||||
// Build and print the validation query
|
||||
countQuery := query.DynamicQuery{
|
||||
From: "users",
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{{Column: "email", Operator: query.OpEqual, Value: "test@example.com"}},
|
||||
}},
|
||||
}
|
||||
sql, args, err := qb.BuildCountQuery(countQuery)
|
||||
if err != nil {
|
||||
log.Printf("Error building validation query: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated Validation SQL: %s\nArgs: %v\n", sql, args)
|
||||
|
||||
isDuplicate, err := validator.Validate(ctx, db, emailRule, userData)
|
||||
if err != nil {
|
||||
log.Printf("Error validasi: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if isDuplicate {
|
||||
fmt.Println("-> Validasi: Email 'test@example.com' sudah ada.")
|
||||
} else {
|
||||
fmt.Println("-> Validasi: Email 'test@example.com' tersedia.")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 9: OPERASI JSON
|
||||
// =============================================================================
|
||||
|
||||
// jsonQueryExample demonstrates JSON operations in queries.
|
||||
// It builds and prints the JSON queries before executing them.
|
||||
// Expected output: Prints JSON SELECT and UPDATE SQL queries, number of employees found, and update success message.
|
||||
func jsonQueryExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
db, err := dbService.GetSQLXDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||
return
|
||||
}
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
query := query.DynamicQuery{
|
||||
Fields: []query.SelectField{{Expression: "*"}},
|
||||
From: "employees",
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
{
|
||||
Column: "metadata",
|
||||
Operator: query.OpJsonEqual,
|
||||
Value: "Engineering",
|
||||
Options: map[string]interface{}{"path": "department"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
var employees []Employee
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
log.Printf("Error building JSON query: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated JSON SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||
err = qb.ExecuteQuery(ctx, db, query, &employees)
|
||||
if err != nil {
|
||||
log.Printf("Error query JSON: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> Operasi JSON: Ditemukan %d employee di departemen Engineering (dari metadata JSON).\n", len(employees))
|
||||
|
||||
updateData := query.UpdateData{
|
||||
JsonUpdates: map[string]query.JsonUpdate{
|
||||
"metadata": {Path: "role", Value: "Senior Developer"},
|
||||
},
|
||||
}
|
||||
filter := []query.FilterGroup{{Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: 1}}}}
|
||||
sql, args, err = qb.BuildUpdateQuery("employees", updateData, filter)
|
||||
if err != nil {
|
||||
log.Printf("Error building JSON update: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated JSON UPDATE SQL: %s\nArgs: %v\n", sql, args)
|
||||
_, err = qb.ExecuteUpdate(ctx, db, "employees", updateData, filter)
|
||||
if err != nil {
|
||||
log.Printf("Error update JSON: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("-> Operasi JSON: Berhasil memperbarui 'role' di metadata untuk employee ID 1.")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 10: QUERY MONGODB
|
||||
// =============================================================================
|
||||
|
||||
// mongodbExample demonstrates MongoDB queries using the query builder.
|
||||
// It prints the built filters and pipelines before executing them.
|
||||
// Expected output: Prints MongoDB find filter, number of active users, aggregation pipeline, and number of departments.
|
||||
func mongodbExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
client, err := dbService.GetMongoClient("mongodb")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan klien MongoDB: %v", err)
|
||||
return
|
||||
}
|
||||
collection := client.Database("company_db").Collection("users")
|
||||
mqb := query.NewMongoQueryBuilder()
|
||||
|
||||
// --- FIND ---
|
||||
fmt.Println("\n--- Operasi FIND ---")
|
||||
findQuery := query.DynamicQuery{
|
||||
Filters: []query.FilterGroup{{Filters: []query.DynamicFilter{{Column: "status", Operator: query.OpEqual, Value: "active"}}}},
|
||||
Limit: 5,
|
||||
}
|
||||
filter, _, _ := mqb.BuildFindQuery(findQuery)
|
||||
fmt.Printf("-> MongoDB Find Filter: %#v\n", filter)
|
||||
|
||||
var users []User
|
||||
err = mqb.ExecuteFind(ctx, collection, findQuery, &users)
|
||||
if err != nil {
|
||||
log.Printf("Error MongoDB Find: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> MongoDB Find: Ditemukan %d user aktif.\n", len(users))
|
||||
|
||||
// --- AGGREGATION ---
|
||||
fmt.Println("\n--- Operasi AGGREGATION ---")
|
||||
aggQuery := query.DynamicQuery{
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "department", Alias: "_id"},
|
||||
{Expression: "COUNT(*)", Alias: "count"},
|
||||
},
|
||||
GroupBy: []string{"department"},
|
||||
}
|
||||
pipeline, _ := mqb.BuildAggregateQuery(aggQuery)
|
||||
fmt.Printf("-> MongoDB Aggregation Pipeline: %#v\n", pipeline)
|
||||
|
||||
var aggResults []struct {
|
||||
ID string `bson:"_id"`
|
||||
Count int `bson:"count"`
|
||||
}
|
||||
err = mqb.ExecuteAggregate(ctx, collection, aggQuery, &aggResults)
|
||||
if err != nil {
|
||||
log.Printf("Error MongoDB Aggregate: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> MongoDB Aggregate: Ditemukan user di %d departemen.\n", len(aggResults))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 11: PENGGUNAAN READ REPLICA
|
||||
// =============================================================================
|
||||
|
||||
// readReplicaExample demonstrates using read replicas for queries.
|
||||
// It builds and prints the count query before executing it on the read replica.
|
||||
// Expected output: Prints COUNT SQL query and the total number of users from the read replica.
|
||||
// Example raw query:
|
||||
// SELECT COUNT(*) FROM users
|
||||
func readReplicaExample(dbService database.Service) {
|
||||
ctx := context.Background()
|
||||
readDB, err := dbService.GetReadDB("main")
|
||||
if err != nil {
|
||||
log.Printf("Gagal mendapatkan read replica: %v", err)
|
||||
return
|
||||
}
|
||||
readxDB := sqlx.NewDb(readDB, "pgx")
|
||||
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||
|
||||
countQuery := query.DynamicQuery{From: "users"}
|
||||
sql, args, err := qb.BuildCountQuery(countQuery)
|
||||
if err != nil {
|
||||
log.Printf("Error building count query: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Generated COUNT SQL: %s\nArgs: %v\n", sql, args)
|
||||
count, err := qb.ExecuteCount(ctx, readxDB, countQuery)
|
||||
if err != nil {
|
||||
log.Printf("Error query di read replica: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("-> Read Replica: Total user (dari read replica): %d\n", count)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 12: HEALTH CHECK DATABASE
|
||||
// =============================================================================
|
||||
|
||||
// healthCheckExample demonstrates database health checks.
|
||||
// It prints the health status of all databases.
|
||||
// Expected output: Prints health status for each database (up/down with type or error).
|
||||
func healthCheckExample(dbService database.Service) {
|
||||
healthStatus := dbService.Health()
|
||||
fmt.Println("-> Health Check Status:")
|
||||
for dbName, status := range healthStatus {
|
||||
if status["status"] == "up" {
|
||||
fmt.Printf(" - Database %s: SEHAT (%s)\n", dbName, status["type"])
|
||||
} else {
|
||||
fmt.Printf(" - Database %s: TIDAK SEHAT - %s\n", dbName, status["error"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH 13: PARSING QUERY DARI URL
|
||||
// =============================================================================
|
||||
|
||||
// urlQueryParsingExample demonstrates parsing query parameters from URL.
|
||||
// It parses the URL query and prints the resulting dynamic query structure.
|
||||
// Expected output: Prints parsed fields, filters, sort, and limit from the URL query.
|
||||
func urlQueryParsingExample(dbService database.Service) {
|
||||
values := url.Values{}
|
||||
values.Set("fields", "id,name")
|
||||
values.Set("filter[status][_eq]", "active")
|
||||
values.Set("filter[age][_gt]", "25")
|
||||
values.Set("sort", "-name")
|
||||
values.Set("limit", "10")
|
||||
|
||||
parser := query.NewQueryParser()
|
||||
dynamicQuery, err := parser.ParseQuery(values, "users")
|
||||
if err != nil {
|
||||
log.Printf("Error parsing URL query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("-> Parsing URL Query:")
|
||||
fmt.Printf(" Fields: %v\n", dynamicQuery.Fields)
|
||||
fmt.Printf(" Filters: %+v\n", dynamicQuery.Filters)
|
||||
fmt.Printf(" Sort: %+v\n", dynamicQuery.Sort)
|
||||
fmt.Printf(" Limit: %d\n", dynamicQuery.Limit)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AKHIR FILE
|
||||
// =============================================================================
|
||||
@@ -0,0 +1,244 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
queryUtils "antrian-operasi/internal/utils/query"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC VALIDATION RULE
|
||||
// =============================================================================
|
||||
|
||||
// ValidationRule mendefinisikan aturan untuk memeriksa duplikat atau kondisi lain.
|
||||
// Struct ini membuat validator dapat digunakan kembali untuk tabel apa pun.
|
||||
type ValidationRule struct {
|
||||
// TableName adalah nama tabel yang akan diperiksa.
|
||||
TableName string
|
||||
|
||||
// UniqueColumns adalah daftar kolom yang, jika digabungkan, harus unik.
|
||||
// Contoh: []string{"email"} atau []string{"first_name", "last_name", "dob"}
|
||||
UniqueColumns []string
|
||||
|
||||
// Conditions adalah filter tambahan yang harus dipenuhi.
|
||||
// Ini sangat berguna untuk aturan bisnis, seperti "status != 'deleted'".
|
||||
// Gunakan queryUtils.DynamicFilter untuk fleksibilitas penuh.
|
||||
Conditions []queryUtils.DynamicFilter
|
||||
|
||||
// ExcludeIDColumn dan ExcludeIDValue digunakan untuk operasi UPDATE,
|
||||
// untuk memastikan bahwa record tidak membandingkan dirinya sendiri.
|
||||
ExcludeIDColumn string
|
||||
ExcludeIDValue interface{}
|
||||
}
|
||||
|
||||
// NewUniqueFieldRule adalah helper untuk membuat aturan validasi unik untuk satu kolom.
|
||||
// Ini adalah cara cepat untuk membuat aturan yang paling umum.
|
||||
func NewUniqueFieldRule(tableName, uniqueColumn string, additionalConditions ...queryUtils.DynamicFilter) ValidationRule {
|
||||
return ValidationRule{
|
||||
TableName: tableName,
|
||||
UniqueColumns: []string{uniqueColumn},
|
||||
Conditions: additionalConditions,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC VALIDATOR
|
||||
// =============================================================================
|
||||
|
||||
// DynamicValidator menyediakan metode untuk menjalankan validasi berdasarkan ValidationRule.
|
||||
// Ini sepenuhnya generik dan tidak terikat pada tabel atau model tertentu.
|
||||
type DynamicValidator struct {
|
||||
qb *queryUtils.QueryBuilder
|
||||
}
|
||||
|
||||
// NewDynamicValidator membuat instance DynamicValidator baru.
|
||||
func NewDynamicValidator(qb *queryUtils.QueryBuilder) *DynamicValidator {
|
||||
return &DynamicValidator{qb: qb}
|
||||
}
|
||||
|
||||
// Validate menjalankan validasi terhadap aturan yang diberikan.
|
||||
// `data` adalah map yang berisi nilai untuk kolom yang akan diperiksa (biasanya dari request body).
|
||||
// Mengembalikan `true` jika ada duplikat yang ditemukan (validasi gagal), `false` jika tidak ada duplikat (validasi berhasil).
|
||||
func (dv *DynamicValidator) Validate(ctx context.Context, db *sqlx.DB, rule ValidationRule, data map[string]interface{}) (bool, error) {
|
||||
// LOGGING: Log validation start
|
||||
fmt.Printf("[VALIDATION] Starting validation for table: %s, unique columns: %v, data: %v\n", rule.TableName, rule.UniqueColumns, data)
|
||||
|
||||
if len(rule.UniqueColumns) == 0 {
|
||||
fmt.Printf("[VALIDATION] ERROR: ValidationRule must have at least one UniqueColumn\n")
|
||||
return false, fmt.Errorf("ValidationRule must have at least one UniqueColumn")
|
||||
}
|
||||
|
||||
// 1. Kumpulkan semua filter dari aturan
|
||||
var allFilters []queryUtils.DynamicFilter
|
||||
|
||||
// Tambahkan kondisi tambahan (misalnya, status != 'deleted')
|
||||
allFilters = append(allFilters, rule.Conditions...)
|
||||
fmt.Printf("[VALIDATION] Added %d condition filters\n", len(rule.Conditions))
|
||||
|
||||
// 2. Bangun filter untuk kolom unik berdasarkan data yang diberikan
|
||||
for _, colName := range rule.UniqueColumns {
|
||||
value, exists := data[colName]
|
||||
if !exists {
|
||||
// Jika data untuk kolom unik tidak ada, ini adalah kesalahan pemrograman.
|
||||
fmt.Printf("[VALIDATION] ERROR: data for unique column '%s' not found in provided data map\n", colName)
|
||||
return false, fmt.Errorf("data for unique column '%s' not found in provided data map", colName)
|
||||
}
|
||||
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||
Column: colName,
|
||||
Operator: queryUtils.OpEqual,
|
||||
Value: value,
|
||||
})
|
||||
fmt.Printf("[VALIDATION] Added filter for column '%s' with value: %v\n", colName, value)
|
||||
}
|
||||
|
||||
// 3. Tambahkan filter pengecualian ID (untuk operasi UPDATE)
|
||||
if rule.ExcludeIDColumn != "" {
|
||||
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||
Column: rule.ExcludeIDColumn,
|
||||
Operator: queryUtils.OpNotEqual,
|
||||
Value: rule.ExcludeIDValue,
|
||||
})
|
||||
fmt.Printf("[VALIDATION] Added exclude filter for column '%s' with value: %v\n", rule.ExcludeIDColumn, rule.ExcludeIDValue)
|
||||
}
|
||||
|
||||
// 4. Bangun dan eksekusi query untuk menghitung jumlah record yang cocok
|
||||
query := queryUtils.DynamicQuery{
|
||||
From: rule.TableName,
|
||||
Filters: []queryUtils.FilterGroup{{Filters: allFilters, LogicOp: "AND"}},
|
||||
}
|
||||
|
||||
fmt.Printf("[VALIDATION] Built query with %d total filters\n", len(allFilters))
|
||||
|
||||
count, err := dv.qb.ExecuteCount(ctx, db, query)
|
||||
if err != nil {
|
||||
fmt.Printf("[VALIDATION] ERROR: failed to execute validation query for table %s: %v\n", rule.TableName, err)
|
||||
return false, fmt.Errorf("failed to execute validation query for table %s: %w", rule.TableName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("[VALIDATION] Query executed successfully, count result: %d\n", count)
|
||||
|
||||
// 5. Kembalikan hasil
|
||||
result := count > 0
|
||||
fmt.Printf("[VALIDATION] Validation result: isDuplicate=%t (count > 0: %d > 0 = %t)\n", result, count, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTOH PENGGUNAAN (UNTUK DITEMPATKAN DI HANDLER ANDA)
|
||||
// =============================================================================
|
||||
|
||||
/*
|
||||
// --- Cara Penggunaan di RetribusiHandler ---
|
||||
|
||||
// 1. Tambahkan DynamicValidator ke struct handler
|
||||
type RetribusiHandler struct {
|
||||
// ...
|
||||
validator *validation.DynamicValidator
|
||||
}
|
||||
|
||||
// 2. Inisialisasi di constructor
|
||||
func NewRetribusiHandler() *RetribusiHandler {
|
||||
qb := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).SetAllowedColumns(...)
|
||||
|
||||
return &RetribusiHandler{
|
||||
// ...
|
||||
validator: validation.NewDynamicValidator(qb),
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Gunakan di CreateRetribusi
|
||||
func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) {
|
||||
var req retribusi.RetribusiCreateRequest
|
||||
// ... bind dan validasi request ...
|
||||
|
||||
// Siapkan aturan validasi: KodeTarif harus unik di antara record yang tidak dihapus.
|
||||
rule := validation.NewUniqueFieldRule(
|
||||
"data_retribusi", // Nama tabel
|
||||
"Kode_tarif", // Kolom yang harus unik
|
||||
queryUtils.DynamicFilter{ // Kondisi tambahan
|
||||
Column: "status",
|
||||
Operator: queryUtils.OpNotEqual,
|
||||
Value: "deleted",
|
||||
},
|
||||
)
|
||||
|
||||
// Siapkan data dari request untuk divalidasi
|
||||
dataToValidate := map[string]interface{}{
|
||||
"Kode_tarif": req.KodeTarif,
|
||||
}
|
||||
|
||||
// Eksekusi validasi
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if isDuplicate {
|
||||
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// ... lanjutkan proses create ...
|
||||
}
|
||||
|
||||
// 4. Gunakan di UpdateRetribusi
|
||||
func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req retribusi.RetribusiUpdateRequest
|
||||
// ... bind dan validasi request ...
|
||||
|
||||
// Siapkan aturan validasi: KodeTarif harus unik, kecuali untuk record dengan ID ini.
|
||||
rule := validation.ValidationRule{
|
||||
TableName: "data_retribusi",
|
||||
UniqueColumns: []string{"Kode_tarif"},
|
||||
Conditions: []queryUtils.DynamicFilter{
|
||||
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
|
||||
},
|
||||
ExcludeIDColumn: "id", // Kecualikan berdasarkan kolom 'id'
|
||||
ExcludeIDValue: id, // ...dengan nilai ID dari parameter
|
||||
}
|
||||
|
||||
dataToValidate := map[string]interface{}{
|
||||
"Kode_tarif": req.KodeTarif,
|
||||
}
|
||||
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if isDuplicate {
|
||||
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// ... lanjutkan proses update ...
|
||||
}
|
||||
|
||||
// --- Contoh Penggunaan untuk Kasus Lain ---
|
||||
|
||||
// Contoh: Validasi kombinasi unik untuk tabel 'users'
|
||||
// (email dan company_id harus unik bersama-sama)
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
// ...
|
||||
|
||||
rule := validation.ValidationRule{
|
||||
TableName: "users",
|
||||
UniqueColumns: []string{"email", "company_id"}, // Unik komposit
|
||||
}
|
||||
|
||||
dataToValidate := map[string]interface{}{
|
||||
"email": req.Email,
|
||||
"company_id": req.CompanyID,
|
||||
}
|
||||
|
||||
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||
// ... handle error dan duplicate
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user