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, } }