update builder
This commit is contained in:
1611
internal/handlers/retribusi/sqruel
Normal file
1611
internal/handlers/retribusi/sqruel
Normal file
File diff suppressed because it is too large
Load Diff
@@ -61,12 +61,23 @@ type SortField struct {
|
|||||||
Order string `json:"order"` // ASC, DESC
|
Order string `json:"order"` // ASC, DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateData represents data for UPDATE operations
|
||||||
|
type UpdateData struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Values []interface{} `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertData represents data for INSERT operations
|
||||||
|
type InsertData struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Values []interface{} `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
// QueryBuilder builds SQL queries from dynamic filters
|
// QueryBuilder builds SQL queries from dynamic filters
|
||||||
type QueryBuilder struct {
|
type QueryBuilder struct {
|
||||||
tableName string
|
tableName string
|
||||||
columnMapping map[string]string // Maps API field names to DB column names
|
columnMapping map[string]string // Maps API field names to DB column names
|
||||||
allowedColumns map[string]bool // Security: only allow specified columns
|
allowedColumns map[string]bool // Security: only allow specified columns
|
||||||
// PERUBAHAN 1: Hapus paramCounter dan mu untuk membuat QueryBuilder stateless dan thread-safe.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewQueryBuilder creates a new query builder instance
|
// NewQueryBuilder creates a new query builder instance
|
||||||
@@ -85,8 +96,6 @@ func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilde
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAllowedColumns sets the list of allowed columns for security
|
// SetAllowedColumns sets the list of allowed columns for security
|
||||||
// PERUBAHAN 3: Nama kolom di sini seharusnya adalah nama kolom ASLI di database
|
|
||||||
// untuk pemeriksaan keamanan yang lebih konsisten.
|
|
||||||
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
|
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
|
||||||
qb.allowedColumns = make(map[string]bool)
|
qb.allowedColumns = make(map[string]bool)
|
||||||
for _, col := range columns {
|
for _, col := range columns {
|
||||||
@@ -95,10 +104,8 @@ func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
|
|||||||
return qb
|
return qb
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildQuery builds the complete SQL query
|
// BuildQuery builds the complete SQL SELECT query
|
||||||
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
|
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
// PERUBAHAN 1: paramCounter sekarang lokal untuk fungsi ini.
|
|
||||||
// Ini membuat QueryBuilder aman untuk digunakan secara konkuren (thread-safe).
|
|
||||||
paramCounter := 0
|
paramCounter := 0
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
||||||
@@ -164,6 +171,197 @@ func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, e
|
|||||||
return sql, args, nil
|
return sql, args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildInsertQuery builds an INSERT query
|
||||||
|
func (qb *QueryBuilder) BuildInsertQuery(data InsertData, returningColumns ...string) (string, []interface{}, error) {
|
||||||
|
if len(data.Columns) == 0 || len(data.Values) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no columns or values provided for INSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Columns) != len(data.Values) {
|
||||||
|
return "", nil, fmt.Errorf("columns and values count mismatch for INSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
paramCounter := 0
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Build column names
|
||||||
|
var columns []string
|
||||||
|
for _, col := range data.Columns {
|
||||||
|
mappedCol := qb.mapAndValidateColumn(col)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
columns = append(columns, fmt.Sprintf(`"%s"`, mappedCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no valid columns provided for INSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build value placeholders
|
||||||
|
var placeholders []string
|
||||||
|
for i := 0; i < len(columns); i++ {
|
||||||
|
paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", paramCounter))
|
||||||
|
args = append(args, data.Values[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build INSERT clause
|
||||||
|
insertClause := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||||
|
qb.tableName,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(placeholders, ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build RETURNING clause if specified
|
||||||
|
returningClause := qb.buildReturningClause(returningColumns)
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{insertClause}
|
||||||
|
if returningClause != "" {
|
||||||
|
sqlParts = append(sqlParts, returningClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildUpdateQuery builds an UPDATE query
|
||||||
|
func (qb *QueryBuilder) BuildUpdateQuery(data UpdateData, filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
|
||||||
|
if len(data.Columns) == 0 || len(data.Values) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no columns or values provided for UPDATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Columns) != len(data.Values) {
|
||||||
|
return "", nil, fmt.Errorf("columns and values count mismatch for UPDATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
paramCounter := 0
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Build SET clause
|
||||||
|
var setParts []string
|
||||||
|
for i, col := range data.Columns {
|
||||||
|
mappedCol := qb.mapAndValidateColumn(col)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
paramCounter++
|
||||||
|
setParts = append(setParts, fmt.Sprintf(`"%s" = $%d`, mappedCol, paramCounter))
|
||||||
|
args = append(args, data.Values[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(setParts) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no valid columns provided for UPDATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
setClause := "SET " + strings.Join(setParts, ", ")
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(filters, ¶mCounter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
|
||||||
|
// Build RETURNING clause if specified
|
||||||
|
returningClause := qb.buildReturningClause(returningColumns)
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{
|
||||||
|
fmt.Sprintf("UPDATE %s", qb.tableName),
|
||||||
|
setClause,
|
||||||
|
}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if returningClause != "" {
|
||||||
|
sqlParts = append(sqlParts, returningClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildDeleteQuery builds a DELETE query
|
||||||
|
func (qb *QueryBuilder) BuildDeleteQuery(filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
|
||||||
|
paramCounter := 0
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Build DELETE clause
|
||||||
|
deleteClause := fmt.Sprintf("DELETE FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(filters, ¶mCounter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
|
||||||
|
// Build RETURNING clause if specified
|
||||||
|
returningClause := qb.buildReturningClause(returningColumns)
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{deleteClause}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if returningClause != "" {
|
||||||
|
sqlParts = append(sqlParts, returningClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCountQuery builds a count query
|
||||||
|
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
|
paramCounter := 0
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Build FROM clause
|
||||||
|
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters, ¶mCounter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
|
||||||
|
// Build GROUP BY clause
|
||||||
|
groupClause := qb.buildGroupByClause(query.GroupBy)
|
||||||
|
|
||||||
|
// Build HAVING clause
|
||||||
|
havingClause, havingArgs, err := qb.buildHavingClause(query.Having, ¶mCounter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
args = append(args, havingArgs...)
|
||||||
|
|
||||||
|
// Combine parts
|
||||||
|
sqlParts := []string{"SELECT COUNT(*)", fromClause}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupClause != "" {
|
||||||
|
sqlParts = append(sqlParts, groupClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildSelectClause builds the SELECT part of the query
|
// buildSelectClause builds the SELECT part of the query
|
||||||
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
||||||
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
|
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
|
||||||
@@ -182,15 +380,9 @@ func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERUBAHAN 3: Lakukan mapping terlebih dahulu, lalu pemeriksaan keamanan.
|
mappedCol := qb.mapAndValidateColumn(field)
|
||||||
mappedCol := field
|
if mappedCol == "" {
|
||||||
if mapped, exists := qb.columnMapping[field]; exists {
|
continue // Skip invalid columns
|
||||||
mappedCol = mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security check: hanya izinkan kolom yang sudah ditentukan (cek nama kolom DB)
|
|
||||||
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[mappedCol] {
|
|
||||||
continue // Lewati kolom yang tidak diizinkan
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, mappedCol))
|
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, mappedCol))
|
||||||
@@ -203,6 +395,55 @@ func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
|||||||
return "SELECT " + strings.Join(selectedFields, ", ")
|
return "SELECT " + strings.Join(selectedFields, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildReturningClause builds the RETURNING part of the query
|
||||||
|
func (qb *QueryBuilder) buildReturningClause(columns []string) string {
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var returningFields []string
|
||||||
|
for _, field := range columns {
|
||||||
|
if field == "*" {
|
||||||
|
returningFields = append(returningFields, "*")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedCol := qb.mapAndValidateColumn(field)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
|
||||||
|
returningFields = append(returningFields, fmt.Sprintf(`"%s"`, mappedCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(returningFields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "RETURNING " + strings.Join(returningFields, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapAndValidateColumn maps a column name and validates it
|
||||||
|
func (qb *QueryBuilder) mapAndValidateColumn(field string) string {
|
||||||
|
// Map the column name
|
||||||
|
mappedCol := field
|
||||||
|
if mapped, exists := qb.columnMapping[field]; exists {
|
||||||
|
mappedCol = mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check: only allow specified columns
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[mappedCol] {
|
||||||
|
return "" // Skip invalid columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security: Validate column name format
|
||||||
|
if !qb.isValidColumnName(mappedCol) {
|
||||||
|
return "" // Skip invalid columns
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
// buildWhereClause builds the WHERE part of the query
|
// buildWhereClause builds the WHERE part of the query
|
||||||
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup, paramCounter *int) (string, []interface{}, error) {
|
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup, paramCounter *int) (string, []interface{}, error) {
|
||||||
if len(filterGroups) == 0 {
|
if len(filterGroups) == 0 {
|
||||||
@@ -213,7 +454,6 @@ func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup, paramCounte
|
|||||||
var allArgs []interface{}
|
var allArgs []interface{}
|
||||||
|
|
||||||
for i, group := range filterGroups {
|
for i, group := range filterGroups {
|
||||||
// PERUBAHAN 2: Tambahkan tanda kurung untuk setiap grup untuk memastikan urutan operasi yang benar.
|
|
||||||
groupCondition, groupArgs, err := qb.buildFilterGroup(group, paramCounter)
|
groupCondition, groupArgs, err := qb.buildFilterGroup(group, paramCounter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
@@ -271,20 +511,10 @@ func (qb *QueryBuilder) buildFilterGroup(group FilterGroup, paramCounter *int) (
|
|||||||
|
|
||||||
// buildFilterCondition builds a single filter condition
|
// buildFilterCondition builds a single filter condition
|
||||||
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter *int) (string, []interface{}, error) {
|
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter *int) (string, []interface{}, error) {
|
||||||
// PERUBAHAN 3: Lakukan mapping terlebih dahulu, lalu pemeriksaan keamanan.
|
// Map and validate the column name
|
||||||
column := filter.Column
|
column := qb.mapAndValidateColumn(filter.Column)
|
||||||
if mappedCol, exists := qb.columnMapping[column]; exists {
|
if column == "" {
|
||||||
column = mappedCol
|
return "", nil, nil // Skip invalid columns
|
||||||
}
|
|
||||||
|
|
||||||
// Security check (cek nama kolom DB hasil mapping)
|
|
||||||
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
|
||||||
return "", nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional security: Validate column name format
|
|
||||||
if !qb.isValidColumnName(column) {
|
|
||||||
return "", nil, fmt.Errorf("invalid column name: %s", column)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap column name in quotes for PostgreSQL
|
// Wrap column name in quotes for PostgreSQL
|
||||||
@@ -292,7 +522,6 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter
|
|||||||
|
|
||||||
switch filter.Operator {
|
switch filter.Operator {
|
||||||
case OpEqual:
|
case OpEqual:
|
||||||
// PERUBAHAN 4: Tangani nilai nil secara eksplisit untuk operator kesetaraan.
|
|
||||||
if filter.Value == nil {
|
if filter.Value == nil {
|
||||||
return fmt.Sprintf("%s IS NULL", column), nil, nil
|
return fmt.Sprintf("%s IS NULL", column), nil, nil
|
||||||
}
|
}
|
||||||
@@ -300,7 +529,6 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter
|
|||||||
return fmt.Sprintf("%s = $%d", column, *paramCounter), []interface{}{filter.Value}, nil
|
return fmt.Sprintf("%s = $%d", column, *paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
case OpNotEqual:
|
case OpNotEqual:
|
||||||
// PERUBAHAN 4: Tangani nilai nil secara eksplisit untuk operator ketidaksamaan.
|
|
||||||
if filter.Value == nil {
|
if filter.Value == nil {
|
||||||
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
|
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
|
||||||
}
|
}
|
||||||
@@ -492,15 +720,9 @@ func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
|
|||||||
|
|
||||||
var orderParts []string
|
var orderParts []string
|
||||||
for _, sort := range sortFields {
|
for _, sort := range sortFields {
|
||||||
// PERUBAHAN 3: Lakukan mapping dan pemeriksaan keamanan.
|
column := qb.mapAndValidateColumn(sort.Column)
|
||||||
column := sort.Column
|
if column == "" {
|
||||||
if mappedCol, exists := qb.columnMapping[column]; exists {
|
continue // Skip invalid columns
|
||||||
column = mappedCol
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security check (cek nama kolom DB hasil mapping)
|
|
||||||
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
order := "ASC"
|
order := "ASC"
|
||||||
@@ -526,15 +748,9 @@ func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string {
|
|||||||
|
|
||||||
var groupParts []string
|
var groupParts []string
|
||||||
for _, field := range groupFields {
|
for _, field := range groupFields {
|
||||||
// PERUBAHAN 3: Lakukan mapping dan pemeriksaan keamanan.
|
column := qb.mapAndValidateColumn(field)
|
||||||
column := field
|
if column == "" {
|
||||||
if mappedCol, exists := qb.columnMapping[column]; exists {
|
continue // Skip invalid columns
|
||||||
column = mappedCol
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security check (cek nama kolom DB hasil mapping)
|
|
||||||
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
|
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
|
||||||
@@ -556,51 +772,6 @@ func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup, paramCount
|
|||||||
return qb.buildWhereClause(havingGroups, paramCounter)
|
return qb.buildWhereClause(havingGroups, paramCounter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildCountQuery builds a count query
|
|
||||||
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
|
|
||||||
// PERUBAHAN 1: paramCounter lokal.
|
|
||||||
paramCounter := 0
|
|
||||||
args := []interface{}{}
|
|
||||||
|
|
||||||
// Build FROM clause
|
|
||||||
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
|
||||||
|
|
||||||
// Build WHERE clause
|
|
||||||
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters, ¶mCounter)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
args = append(args, whereArgs...)
|
|
||||||
|
|
||||||
// Build GROUP BY clause
|
|
||||||
groupClause := qb.buildGroupByClause(query.GroupBy)
|
|
||||||
|
|
||||||
// Build HAVING clause
|
|
||||||
havingClause, havingArgs, err := qb.buildHavingClause(query.Having, ¶mCounter)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
args = append(args, havingArgs...)
|
|
||||||
|
|
||||||
// Combine parts
|
|
||||||
sqlParts := []string{"SELECT COUNT(*)", fromClause}
|
|
||||||
|
|
||||||
if whereClause != "" {
|
|
||||||
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if groupClause != "" {
|
|
||||||
sqlParts = append(sqlParts, groupClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if havingClause != "" {
|
|
||||||
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
sql := strings.Join(sqlParts, " ")
|
|
||||||
return sql, args, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidColumnName validates column name format to prevent SQL injection
|
// isValidColumnName validates column name format to prevent SQL injection
|
||||||
func (qb *QueryBuilder) isValidColumnName(column string) bool {
|
func (qb *QueryBuilder) isValidColumnName(column string) bool {
|
||||||
if column == "" {
|
if column == "" {
|
||||||
@@ -608,7 +779,6 @@ func (qb *QueryBuilder) isValidColumnName(column string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow only alphanumeric characters, underscores, and dots (for table.column format)
|
// Allow only alphanumeric characters, underscores, and dots (for table.column format)
|
||||||
// This is more restrictive than before for better security
|
|
||||||
for _, r := range column {
|
for _, r := range column {
|
||||||
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
||||||
(r >= '0' && r <= '9') || r == '_' || r == '.') {
|
(r >= '0' && r <= '9') || r == '_' || r == '.') {
|
||||||
@@ -632,3 +802,96 @@ func (qb *QueryBuilder) isValidColumnName(column string) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildUpsertQuery builds an UPSERT (INSERT ... ON CONFLICT UPDATE) query for PostgreSQL
|
||||||
|
func (qb *QueryBuilder) BuildUpsertQuery(data InsertData, conflictColumns []string, updateData UpdateData, returningColumns ...string) (string, []interface{}, error) {
|
||||||
|
if len(data.Columns) == 0 || len(data.Values) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no columns or values provided for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Columns) != len(data.Values) {
|
||||||
|
return "", nil, fmt.Errorf("columns and values count mismatch for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conflictColumns) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no conflict columns provided for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
paramCounter := 0
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Build column names
|
||||||
|
var columns []string
|
||||||
|
for _, col := range data.Columns {
|
||||||
|
mappedCol := qb.mapAndValidateColumn(col)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
columns = append(columns, fmt.Sprintf(`"%s"`, mappedCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no valid columns provided for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build value placeholders
|
||||||
|
var placeholders []string
|
||||||
|
for i := 0; i < len(columns); i++ {
|
||||||
|
paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", paramCounter))
|
||||||
|
args = append(args, data.Values[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build INSERT clause
|
||||||
|
insertClause := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||||
|
qb.tableName,
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
strings.Join(placeholders, ", "),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build ON CONFLICT clause
|
||||||
|
var conflictCols []string
|
||||||
|
for _, col := range conflictColumns {
|
||||||
|
mappedCol := qb.mapAndValidateColumn(col)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
conflictCols = append(conflictCols, fmt.Sprintf(`"%s"`, mappedCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conflictCols) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no valid conflict columns provided for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
conflictClause := fmt.Sprintf("ON CONFLICT (%s)", strings.Join(conflictCols, ", "))
|
||||||
|
|
||||||
|
// Build UPDATE clause
|
||||||
|
var updateParts []string
|
||||||
|
for i, col := range updateData.Columns {
|
||||||
|
mappedCol := qb.mapAndValidateColumn(col)
|
||||||
|
if mappedCol == "" {
|
||||||
|
continue // Skip invalid columns
|
||||||
|
}
|
||||||
|
paramCounter++
|
||||||
|
updateParts = append(updateParts, fmt.Sprintf(`"%s" = $%d`, mappedCol, paramCounter))
|
||||||
|
args = append(args, updateData.Values[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateParts) == 0 {
|
||||||
|
return "", nil, fmt.Errorf("no valid update columns provided for UPSERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClause := "DO UPDATE SET " + strings.Join(updateParts, ", ")
|
||||||
|
|
||||||
|
// Build RETURNING clause if specified
|
||||||
|
returningClause := qb.buildReturningClause(returningColumns)
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{insertClause, conflictClause, updateClause}
|
||||||
|
if returningClause != "" {
|
||||||
|
sqlParts = append(sqlParts, returningClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|||||||
1139
internal/utils/query/builder.go
Normal file
1139
internal/utils/query/builder.go
Normal file
File diff suppressed because it is too large
Load Diff
220
internal/utils/query/exemple.go
Normal file
220
internal/utils/query/exemple.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"your_module_path/utils" // Ganti dengan path modul Anda
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// --- 1. Setup QueryBuilder ---
|
||||||
|
// Kita akan menggunakan PostgreSQL untuk contoh ini.
|
||||||
|
// Untuk database lain, cukup ganti DBTypePostgreSQL menjadi DBTypeMySQL, dll.
|
||||||
|
qb := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
|
||||||
|
SetColumnMapping(map[string]string{
|
||||||
|
// API Field -> DB Column
|
||||||
|
"userId": "id",
|
||||||
|
"username": "user_name",
|
||||||
|
"email": "email_address",
|
||||||
|
"isActive": "is_active",
|
||||||
|
"createdAt": "created_at",
|
||||||
|
}).
|
||||||
|
SetAllowedColumns([]string{
|
||||||
|
"id", "user_name", "email_address", "is_active", "created_at", "updated_at",
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println("=== QUERY BUILDER EXAMPLES ===\n")
|
||||||
|
|
||||||
|
// --- 2. CREATE (INSERT) ---
|
||||||
|
fmt.Println("--- CREATE (INSERT) ---")
|
||||||
|
|
||||||
|
// Contoh data user baru
|
||||||
|
newUser := utils.InsertData{
|
||||||
|
Columns: []string{"username", "email", "is_active"},
|
||||||
|
Values: []interface{}{"john_doe", "john.doe@example.com", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build INSERT query
|
||||||
|
sql, args, err := qb.BuildInsertQuery(newUser, "id", "created_at")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build INSERT query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: INSERT INTO users (user_name, email_address, is_active) VALUES ($1, $2, $3) RETURNING id, created_at
|
||||||
|
// Args: [john_doe john.doe@example.com true]
|
||||||
|
|
||||||
|
// --- 3. UPDATE ---
|
||||||
|
fmt.Println("--- UPDATE ---")
|
||||||
|
|
||||||
|
// Data yang akan diupdate untuk user dengan ID 1
|
||||||
|
updateData := utils.UpdateData{
|
||||||
|
Columns: []string{"username", "is_active"},
|
||||||
|
Values: []interface{}{"john_doe_updated", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter untuk menentukan user mana yang akan diupdate
|
||||||
|
filters := []utils.FilterGroup{{
|
||||||
|
Filters: []utils.DynamicFilter{{
|
||||||
|
Column: "userId",
|
||||||
|
Operator: utils.OpEqual,
|
||||||
|
Value: 1,
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Build UPDATE query
|
||||||
|
sql, args, err = qb.BuildUpdateQuery(updateData, filters, "updated_at")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build UPDATE query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: UPDATE "users" SET "user_name" = $1, "is_active" = $2 WHERE ("id" = $3) RETURNING updated_at
|
||||||
|
// Args: [john_doe_updated false 1]
|
||||||
|
|
||||||
|
// --- 4. DELETE ---
|
||||||
|
fmt.Println("--- DELETE ---")
|
||||||
|
|
||||||
|
// Filter untuk menghapus user yang tidak aktif
|
||||||
|
deleteFilters := []utils.FilterGroup{{
|
||||||
|
Filters: []utils.DynamicFilter{{
|
||||||
|
Column: "isActive",
|
||||||
|
Operator: utils.OpEqual,
|
||||||
|
Value: false,
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Build DELETE query
|
||||||
|
sql, args, err = qb.BuildDeleteQuery(deleteFilters, "id")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build DELETE query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: DELETE FROM "users" WHERE ("is_active" = $1) RETURNING id
|
||||||
|
// Args: [false]
|
||||||
|
|
||||||
|
// --- 5. UPSERT (INSERT ... ON CONFLICT) ---
|
||||||
|
fmt.Println("--- UPSERT ---")
|
||||||
|
|
||||||
|
// Data untuk upsert (insert atau update jika sudah ada)
|
||||||
|
upsertData := utils.InsertData{
|
||||||
|
Columns: []string{"id", "username", "email"},
|
||||||
|
Values: []interface{}{1, "unique_user", "unique@example.com"}, // ID 1 mungkin sudah ada
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kolom yang menjadi penentu konflik (misalnya, primary key atau unique key)
|
||||||
|
conflictColumns := []string{"id"}
|
||||||
|
|
||||||
|
// Data yang akan diupdate jika terjadi konflik
|
||||||
|
upsertUpdateData := utils.UpdateData{
|
||||||
|
Columns: []string{"username", "email"},
|
||||||
|
Values: []interface{}{"unique_user_updated", "updated@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build UPSERT query
|
||||||
|
sql, args, err = qb.BuildUpsertQuery(upsertData, conflictColumns, upsertUpdateData, "updated_at")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build UPSERT query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: INSERT INTO users (id, user_name, email_address) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET "user_name" = EXCLUDED.user_name, "email_address" = EXCLUDED.email_address RETURNING updated_at
|
||||||
|
// Args: [1 unique_user unique@example.com unique_user_updated updated@example.com]
|
||||||
|
|
||||||
|
// --- 6. SELECT dengan JOIN ---
|
||||||
|
fmt.Println("--- SELECT with JOIN ---")
|
||||||
|
|
||||||
|
// Reset builder untuk query baru
|
||||||
|
qbJoin := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
|
||||||
|
SetColumnMapping(map[string]string{"userId": "id", "username": "user_name"}).
|
||||||
|
SetAllowedColumns([]string{"id", "user_name", "profile_id"}).
|
||||||
|
Join("INNER", "profiles", "users.id = profiles.user_id") // Tambahkan JOIN
|
||||||
|
|
||||||
|
// Query untuk mendapatkan user dan profilnya
|
||||||
|
joinQuery := utils.DynamicQuery{
|
||||||
|
Fields: []string{"users.id", "user_name", "profiles.bio"},
|
||||||
|
Filters: []utils.FilterGroup{{
|
||||||
|
Filters: []utils.DynamicFilter{{
|
||||||
|
Column: "isActive",
|
||||||
|
Operator: utils.OpEqual,
|
||||||
|
Value: true,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args, err = qbJoin.BuildQuery(joinQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build JOIN query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: SELECT "users"."id", "user_name", "profiles"."bio" FROM "users" INNER JOIN "profiles" ON users.id = profiles.user_id WHERE ("is_active" = $1) LIMIT 10
|
||||||
|
// Args: [true]
|
||||||
|
|
||||||
|
// --- 7. SELECT dengan UNION ---
|
||||||
|
fmt.Println("--- SELECT with UNION ---")
|
||||||
|
|
||||||
|
// Buat dua query builder untuk dua tabel berbeda
|
||||||
|
qbActive := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL)
|
||||||
|
qbArchived := utils.NewQueryBuilder("archived_users", utils.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
// Query pertama: user aktif dari tabel 'users'
|
||||||
|
query1 := qbActive.sqlBuilder.Select("id", "user_name", "'active' as status").Where("is_active = ?", true)
|
||||||
|
|
||||||
|
// Query kedua: user aktif dari tabel 'archived_users'
|
||||||
|
query2 := qbArchived.sqlBuilder.Select("id", "user_name", "'archived' as status").Where("is_active = ?", true)
|
||||||
|
|
||||||
|
// Gabungkan dengan UNION
|
||||||
|
unionQuery := query1.Union(query2)
|
||||||
|
|
||||||
|
sql, args, err = unionQuery.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build UNION query: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: SELECT id, user_name, 'active' as status FROM users WHERE is_active = $1 UNION SELECT id, user_name, 'archived' as status FROM archived_users WHERE is_active = $2
|
||||||
|
// Args: [true true]
|
||||||
|
|
||||||
|
// --- 8. Integrasi dengan QueryParser (Web Context) ---
|
||||||
|
fmt.Println("--- Integration with QueryParser ---")
|
||||||
|
|
||||||
|
// Simulasi query parameter dari URL: /users?fields=id,username&filter[username][_contains]=john&sort=-createdAt&limit=5
|
||||||
|
mockURLValues := url.Values{
|
||||||
|
"fields": []string{"id", "username"},
|
||||||
|
"filter[username][_contains]": []string{"john"},
|
||||||
|
"sort": []string{"-createdAt"},
|
||||||
|
"limit": []string{"5"},
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := utils.NewQueryParser()
|
||||||
|
dynamicQuery, err := parser.ParseQuery(mockURLValues)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse query: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query dari hasil parsing
|
||||||
|
qbParser := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
|
||||||
|
SetColumnMapping(map[string]string{"username": "user_name", "createdAt": "created_at"}).
|
||||||
|
SetAllowedColumns([]string{"id", "user_name", "created_at"})
|
||||||
|
|
||||||
|
sql, args, err = qbParser.BuildQuery(dynamicQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build query from parser: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("SQL: %s\n", sql)
|
||||||
|
fmt.Printf("Args: %v\n\n", args)
|
||||||
|
// Output (PostgreSQL):
|
||||||
|
// SQL: SELECT "id", "user_name" FROM "users" WHERE ("user_name" ILIKE $1) ORDER BY "created_at" DESC LIMIT 5
|
||||||
|
// Args: [%john%]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user