Files
service_antrean/internal/handlers/component/rol_component.go
2026-01-05 11:56:19 +07:00

521 lines
17 KiB
Go

package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models"
componentModels "api-service/internal/models/component"
queryUtils "api-service/internal/utils/query"
"api-service/internal/utils/validation"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
)
// =============================================================================
// GLOBAL INITIALIZATION & VALIDATION
// =============================================================================
var (
db database.Service
once sync.Once
validate *validator.Validate
)
// Initialize the database connection and validator once
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for rol_pages status
func validateRol_pagesStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// =============================================================================
// CACHE IMPLEMENTATION
// =============================================================================
// CacheEntry represents an entry in the cache
type CacheEntry struct {
Data interface{}
ExpiresAt time.Time
}
// IsExpired checks if the cache entry has expired
func (e *CacheEntry) IsExpired() bool {
return time.Now().After(e.ExpiresAt)
}
// InMemoryCache implements a simple in-memory cache with TTL
type InMemoryCache struct {
items sync.Map
}
// NewInMemoryCache creates a new in-memory cache
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{}
}
// Get retrieves an item from the cache
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
val, ok := c.items.Load(key)
if !ok {
return nil, false
}
entry, ok := val.(*CacheEntry)
if !ok || entry.IsExpired() {
c.items.Delete(key)
return nil, false
}
return entry.Data, true
}
// Set stores an item in the cache with a TTL
func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) {
entry := &CacheEntry{
Data: value,
ExpiresAt: time.Now().Add(ttl),
}
c.items.Store(key, entry)
}
// Delete removes an item from the cache
func (c *InMemoryCache) Delete(key string) {
c.items.Delete(key)
}
// DeleteByPrefix removes all items with a specific prefix
func (c *InMemoryCache) DeleteByPrefix(prefix string) {
c.items.Range(func(key, value interface{}) bool {
if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix {
c.items.Delete(key)
}
return true
})
}
// =============================================================================
// ROL_COMPONENT HANDLER STRUCT
// =============================================================================
// Rol_componentHandler handles rol_component services
type Rol_componentHandler struct {
db database.Service
queryBuilder *queryUtils.QueryBuilder
validator *validation.DynamicValidator
cache *InMemoryCache // Re-using the same cache type
}
// NewRol_componentHandler creates a new Rol_componentHandler
func NewRol_componentHandler() *Rol_componentHandler {
// Initialize QueryBuilder with allowed columns for security.
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
SetAllowedColumns([]string{
"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id",
})
return &Rol_componentHandler{
// We reuse the global 'db' instance from the init() function in rol_pages_handler.go
db: db,
queryBuilder: queryBuilder,
validator: validation.NewDynamicValidator(queryBuilder),
cache: NewInMemoryCache(), // Each handler gets its own cache instance
}
}
// =============================================================================
// HANDLER ENDPOINTS
// =============================================================================
// GetRol_components godoc
// @Summary Get Components List
// @Description Get list of components with pagination and filters
// @Tags Components
// @Accept json
// @Produce json
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Param page_id query int false "Filter by parent page ID"
// @Param active query string false "Filter by status"
// @Success 200 {object} componentModels.ComponentsGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /component [get]
func (h *Rol_componentHandler) GetRol_components(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
query := queryUtils.DynamicQuery{
From: "role_access.rol_component",
Aliases: "rc",
Fields: []queryUtils.SelectField{
{Expression: "rc.id", Alias: "id"},
{Expression: "rc.name", Alias: "name"},
{Expression: "rc.description", Alias: "description"},
{Expression: "rc.directory", Alias: "directory"},
{Expression: "rc.active", Alias: "active"},
{Expression: "rc.sort", Alias: "sort"},
{Expression: "rc.fk_rol_pages_id", Alias: "fk_rol_pages_id"},
},
Sort: []queryUtils.SortField{
{Column: "rc.fk_rol_pages_id", Order: "ASC"},
{Column: "rc.sort", Order: "ASC"},
},
}
// Parse pagination
if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 {
query.Limit = limit
}
if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 {
query.Offset = offset
}
dbConn, err := h.db.GetSQLXDB("db_antrean")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Parse filters
var filters []queryUtils.DynamicFilter
if pageID := c.Query("page_id"); pageID != "" {
if id, err := strconv.ParseInt(pageID, 10, 64); err == nil {
filters = append(filters, queryUtils.DynamicFilter{Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id})
}
}
if active := c.Query("active"); active != "" && models.IsValidStatus(active) {
filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active})
}
if len(filters) > 0 {
query.Filters = []queryUtils.FilterGroup{{Filters: filters, LogicOp: "AND"}}
}
// Execute query
var components []componentModels.Rol_component
err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &components)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Get total count for pagination
total, err := h.getTotalCount(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, total)
response := componentModels.ComponentsGetResponse{
Message: "Data rol_component berhasil diambil",
Data: components,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}
// CreateRol_component godoc
// @Summary Create Component
// @Description Create a new component
// @Tags Components
// @Accept json
// @Produce json
// @Param request body componentModels.ComponentCreateRequest true "Component creation request"
// @Success 201 {object} componentModels.ComponentCreateResponse "Component created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /component [post]
func (h *Rol_componentHandler) CreateRol_component(c *gin.Context) {
var req componentModels.ComponentCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetSQLXDB("db_antrean")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
data := queryUtils.InsertData{
Columns: []string{"name", "description", "directory", "active", "sort", "fk_rol_pages_id"},
Values: []interface{}{req.Name, req.Description, req.Directory, req.Active, req.Sort, req.FkRolPagesID},
}
returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"}
sql, args, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", data, returningCols...)
if err != nil {
h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError)
return
}
var dataComponent componentModels.Rol_component
err = dbConn.GetContext(ctx, &dataComponent, sql, args...)
if err != nil {
h.logAndRespondError(c, "Failed to create rol_component", err, http.StatusInternalServerError)
return
}
h.invalidateRelatedCache()
response := componentModels.ComponentCreateResponse{Message: "Rol_component berhasil dibuat", Data: &dataComponent}
c.JSON(http.StatusCreated, response)
}
// UpdateRol_component godoc
// @Summary Update Component
// @Description Update an existing component
// @Tags Components
// @Accept json
// @Produce json
// @Param id path string true "Component ID"
// @Param request body componentModels.ComponentUpdateRequest true "Component update request"
// @Success 200 {object} componentModels.ComponentUpdateResponse "Component updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Component not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /component/{id} [put]
func (h *Rol_componentHandler) UpdateRol_component(c *gin.Context) {
id := c.Param("id")
if id == "" {
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
return
}
var req componentModels.ComponentUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
idInt, err := strconv.Atoi(id)
if err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
req.ID = &idInt
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetSQLXDB("db_antrean")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
columns := make([]string, 0, 6)
values := make([]interface{}, 0, 6)
if req.Name != nil {
columns = append(columns, "name")
values = append(values, *req.Name)
}
if req.Description != nil {
columns = append(columns, "description")
values = append(values, *req.Description) // keep as string; if you want SQL NULL for empty use sql.NullString
}
if req.Directory != nil {
columns = append(columns, "directory")
values = append(values, *req.Directory)
}
if req.Active != nil {
columns = append(columns, "active")
values = append(values, *req.Active)
}
if req.Sort != nil {
columns = append(columns, "sort")
values = append(values, *req.Sort)
}
if req.FkRolPagesID != nil {
columns = append(columns, "fk_rol_pages_id")
values = append(values, *req.FkRolPagesID)
}
if len(columns) == 0 {
h.respondError(c, "No fields to update", fmt.Errorf("empty update payload"), http.StatusBadRequest)
return
}
updateData := queryUtils.UpdateData{
Columns: columns,
Values: values,
}
filters := []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: req.ID},
},
LogicOp: "AND",
}}
returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"}
sql, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", updateData, filters, returningCols...)
if err != nil {
h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError)
return
}
var dataComponent componentModels.Rol_component
err = dbConn.GetContext(ctx, &dataComponent, sql, args...)
if err != nil {
if err.Error() == "sql: no rows in result set" {
h.respondError(c, "Rol_component not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update rol_component", err, http.StatusInternalServerError)
}
return
}
h.invalidateRelatedCache()
response := componentModels.ComponentUpdateResponse{
Message: "Rol_component berhasil diperbarui",
Data: &dataComponent,
}
c.JSON(http.StatusOK, response)
}
// DeleteRol_component godoc
// @Summary Delete Component
// @Description Delete a component
// @Tags Components
// @Accept json
// @Produce json
// @Param id path string true "Component ID"
// @Success 200 {object} componentModels.ComponentDeleteResponse "Component deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Component not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /component/{id} [delete]
func (h *Rol_componentHandler) DeleteRol_component(c *gin.Context) {
id := c.Param("id")
if id == "" {
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
return
}
dbConn, err := h.db.GetSQLXDB("db_antrean")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
// Soft delete by changing status
updateData := queryUtils.UpdateData{
Columns: []string{"active"},
Values: []interface{}{false},
}
filters := []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
{Column: "active", Operator: queryUtils.OpNotEqual, Value: false},
},
LogicOp: "AND",
}}
result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "role_access.rol_component", updateData, filters)
if err != nil {
h.logAndRespondError(c, "Failed to delete rol_component", err, http.StatusInternalServerError)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
h.respondError(c, "Rol_component not found", sql.ErrNoRows, http.StatusNotFound)
return
}
h.invalidateRelatedCache()
response := componentModels.ComponentDeleteResponse{Message: "Rol_component berhasil dihapus", ID: id}
c.JSON(http.StatusOK, response)
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
// invalidateRelatedCache clears cache entries that might be affected by component changes.
// This includes the pages cache, since pages contain components.
func (h *Rol_componentHandler) invalidateRelatedCache() {
h.cache.DeleteByPrefix("rol_component:")
h.cache.DeleteByPrefix("rol_pages:") // Invalidate pages cache as well
}
func (h *Rol_componentHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) {
countQuery := queryUtils.DynamicQuery{
From: query.From,
Aliases: query.Aliases,
Filters: query.Filters,
}
count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery)
if err != nil {
return 0, fmt.Errorf("failed to execute count query: %w", err)
}
return int(count), nil
}
func (h *Rol_componentHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode})
h.respondError(c, message, err, statusCode)
}
func (h *Rol_componentHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()})
}
func (h *Rol_componentHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages, currentPage := 0, 1
if limit > 0 {
totalPages = (total + limit - 1) / limit
currentPage = (offset / limit) + 1
}
return models.MetaResponse{
Limit: limit, Offset: offset, Total: total, TotalPages: totalPages,
CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0,
}
}