forked from rachmadiyanti.annisa.3004/service_antrean
521 lines
17 KiB
Go
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,
|
|
}
|
|
}
|