From 5b81c2e00a952cca208c3908c72c52b919f5e1ba Mon Sep 17 00:00:00 2001 From: Annisa Rachmadiyanti Date: Mon, 5 Jan 2026 15:16:57 +0700 Subject: [PATCH] Code Structure --- internal/handlers/pages/rol_pages.go | 1790 +++++++++++++++----------- internal/models/pages/rol_pages.go | 6 +- internal/routes/v1/routes.go | 1 + 3 files changed, 1020 insertions(+), 777 deletions(-) diff --git a/internal/handlers/pages/rol_pages.go b/internal/handlers/pages/rol_pages.go index 056f8f2..157fac0 100644 --- a/internal/handlers/pages/rol_pages.go +++ b/internal/handlers/pages/rol_pages.go @@ -15,6 +15,7 @@ import ( "fmt" "io" "net/http" + "reflect" "strconv" "strings" "sync" @@ -36,6 +37,15 @@ var ( validate *validator.Validate ) +var ( + pageFieldToColumn = map[string]string{ + "Name": "name", "Icon": "icon", "Url": "url", "Level": "level", "Sort": "sort", + } + componentFieldToColumn = map[string]string{ + "Name": "name", "Description": "description", "Directory": "directory", "Sort": "sort", "Active": "active", + } +) + // Initialize the database connection and validator once func init() { once.Do(func() { @@ -48,7 +58,7 @@ func init() { }) } -// Custom validation for rol_pages status +// Custom validator for rol_pages status field func validateRol_pagesStatus(fl validator.FieldLevel) bool { return models.IsValidStatus(fl.Field().String()) } @@ -57,7 +67,7 @@ func validateRol_pagesStatus(fl validator.FieldLevel) bool { // CACHE IMPLEMENTATION // ============================================================================= -// CacheEntry represents an entry in the cache +// Simple in-memory cache with expiration type CacheEntry struct { Data interface{} ExpiresAt time.Time @@ -68,7 +78,7 @@ func (e *CacheEntry) IsExpired() bool { return time.Now().After(e.ExpiresAt) } -// InMemoryCache implements a simple in-memory cache with TTL +// InMemoryCache is a simple in-memory cache type InMemoryCache struct { items sync.Map } @@ -96,11 +106,10 @@ func (c *InMemoryCache) Get(key string) (interface{}, bool) { // Set stores an item in the cache with a TTL func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { - entry := &CacheEntry{ + c.items.Store(key, &CacheEntry{ Data: value, ExpiresAt: time.Now().Add(ttl), - } - c.items.Store(key, entry) + }) } // Delete removes an item from the cache @@ -108,10 +117,10 @@ func (c *InMemoryCache) Delete(key string) { c.items.Delete(key) } -// DeleteByPrefix removes all items with a specific prefix +// DeleteByPrefix removes all items with keys that start with the given 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 { + if keyStr, ok := key.(string); ok && strings.HasPrefix(keyStr, prefix) { c.items.Delete(key) } return true @@ -119,10 +128,10 @@ func (c *InMemoryCache) DeleteByPrefix(prefix string) { } // ============================================================================= -// Rol_pages HANDLER STRUCT +// ROL_PAGES HANDLER // ============================================================================= -// Rol_pagesHandler handles rol_pages services +// Rol_pagesHandler struct type Rol_pagesHandler struct { db database.Service queryBuilder *queryUtils.QueryBuilder @@ -130,9 +139,9 @@ type Rol_pagesHandler struct { cache *InMemoryCache } -// NewRol_pagesHandler creates a new Rol_pagesHandler with a pre-configured QueryBuilder +// NewRol_pagesHandler creates a new Rol_pagesHandler func NewRol_pagesHandler() *Rol_pagesHandler { - // Initialize QueryBuilder with allowed columns list for security. + // Initialize the query builder queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL). SetAllowedColumns([]string{ "id", "name", "icon", "url", "level", "sort", "parent", "active", @@ -173,6 +182,7 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() + // Build base query query := queryUtils.DynamicQuery{ From: "role_access.rol_pages", Aliases: "rp", @@ -205,10 +215,7 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { {Expression: "rper.role_keycloak", Alias: "permission_role_keycloak"}, {Expression: "rper.group_keycloak", Alias: "permission_group_keycloak"}, }, - Sort: []queryUtils.SortField{ - {Column: "rp.sort", Order: "ASC"}, - // {Column: "rp.id", Order: "ASC"}, - }, + Sort: []queryUtils.SortField{{Column: "rp.sort", Order: "ASC"}}, Joins: []queryUtils.Join{ { Type: "LEFT", @@ -234,13 +241,9 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { } // 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 - } + h.parsePagination(c, &query) + // Get database connection dbConn, err := h.db.GetSQLXDB("db_antrean") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) @@ -249,14 +252,14 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { // Parse filters var filters []queryUtils.DynamicFilter - // accept active from path param or query param; only boolean true/false allowed - activeVal := "" - if p := c.Param("active"); p != "" { - activeVal = p - } else if q := c.Query("active"); q != "" { - activeVal = q - } - if activeVal != "" { + if activeVal := c.Param("active"); activeVal != "" { + if b, err := strconv.ParseBool(activeVal); err == nil { + filters = append(filters, queryUtils.DynamicFilter{Column: "rp.active", Operator: queryUtils.OpEqual, Value: b}) + } else { + h.respondError(c, "Invalid 'active' value; must be true or false", fmt.Errorf("invalid active value: %s", activeVal), http.StatusBadRequest) + return + } + } else if activeVal := c.Query("active"); activeVal != "" { if b, err := strconv.ParseBool(activeVal); err == nil { filters = append(filters, queryUtils.DynamicFilter{Column: "rp.active", Operator: queryUtils.OpEqual, Value: b}) } else { @@ -265,11 +268,9 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { } } - // Parse roles and groups params (comma separated). They filter on permission fields (rper). + // Parse roles and groups params if rolesParam := c.Query("roles"); rolesParam != "" { - roles := parseCSVParam(rolesParam) - if len(roles) > 0 { - // Use array-overlap operator so record's role_keycloak array overlaps provided roles + if roles := parseCSVParam(rolesParam); len(roles) > 0 { filters = append(filters, queryUtils.DynamicFilter{ Column: "rper.role_keycloak", Operator: queryUtils.OpArrayOverlap, @@ -278,8 +279,7 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { } } if groupsParam := c.Query("groups"); groupsParam != "" { - groups := parseCSVParam(groupsParam) - if len(groups) > 0 { + if groups := parseCSVParam(groupsParam); len(groups) > 0 { filters = append(filters, queryUtils.DynamicFilter{ Column: "rper.group_keycloak", Operator: queryUtils.OpArrayOverlap, @@ -301,10 +301,7 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { cacheKey = fmt.Sprintf("rol_pages:search:%s:%d:%d", search, query.Limit, query.Offset) searchFilters = []queryUtils.DynamicFilter{ - {Column: "rp.id", Operator: queryUtils.OpEqual, Value: "" + search + ""}, - // {Column: "rp.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, - // {Column: "rp.url", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, - // {Column: "rp.level", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + {Column: "rp.id", Operator: queryUtils.OpEqual, Value: search}, } if cachedData, found := h.cache.Get(cacheKey); found { @@ -411,32 +408,12 @@ func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /pages [post] func (h *Rol_pagesHandler) CreateRol_pages(c *gin.Context) { - // Read raw body to support either single object or array - body, err := io.ReadAll(c.Request.Body) + // Parse request body (supports both single object and array) + reqs, err := h.parseCreateRequest(c) if err != nil { h.respondError(c, "Invalid request body", err, http.StatusBadRequest) return } - trimmed := bytes.TrimSpace(body) - if len(trimmed) == 0 { - h.respondError(c, "Empty request body", fmt.Errorf("empty body"), http.StatusBadRequest) - return - } - - var reqs []pagesModels.PagesCreateRequest - if trimmed[0] == '[' { - if err := json.Unmarshal(trimmed, &reqs); err != nil { - h.respondError(c, "Invalid request body (array)", err, http.StatusBadRequest) - return - } - } else { - var single pagesModels.PagesCreateRequest - if err := json.Unmarshal(trimmed, &single); err != nil { - h.respondError(c, "Invalid request body (object)", err, http.StatusBadRequest) - return - } - reqs = append(reqs, single) - } // Validate each request for i := range reqs { @@ -451,140 +428,27 @@ func (h *Rol_pagesHandler) CreateRol_pages(c *gin.Context) { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } + ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Second) defer cancel() // Pre-validate unique IDs (if provided) for i, req := range reqs { if req.ID != nil { - rule := validation.NewUniqueFieldRule( - "role_access.rol_pages", - "id", - queryUtils.DynamicFilter{ - Column: "active", - Operator: queryUtils.OpNotEqual, - Value: false, - }, - ) - dataToValidate := map[string]interface{}{"id": *req.ID} - isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) - if err != nil { + if isDuplicate, err := h.validateUniqueID(ctx, dbConn, *req.ID, ""); err != nil { h.logAndRespondError(c, fmt.Sprintf("Failed to validate id at index %d", i), err, http.StatusInternalServerError) return - } - if isDuplicate { + } else if isDuplicate { h.respondError(c, fmt.Sprintf("id already exists at index %d", i), fmt.Errorf("duplicate id: %d", *req.ID), http.StatusConflict) return } } } - // Begin single transaction for all inserts (atomic) - tx, err := dbConn.BeginTxx(ctx, nil) + // Execute creation in a transaction + createdPages, err := h.executeCreateTransaction(ctx, dbConn, reqs) if err != nil { - h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) - return - } - rollback := func(errMsg string, err error, code int) { - _ = tx.Rollback() - h.logAndRespondError(c, errMsg, err, code) - } - - createdPages := make([]pagesModels.Rol_pages, 0, len(reqs)) - - for idx, req := range reqs { - // Insert page - pageInsert := queryUtils.InsertData{ - Columns: []string{"name", "icon", "url", "level", "sort", "parent", "active"}, - Values: []interface{}{req.Name, req.Icon, req.Url, req.Level, req.Sort, req.Parent, req.Active}, - } - pageReturning := []string{"id", "name", "icon", "url", "level", "sort", "parent", "active"} - pageSQL, pageArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_pages", pageInsert, pageReturning...) - if err != nil { - rollback("Failed to build insert query for page", err, http.StatusInternalServerError) - return - } - - var createdPage pagesModels.Rol_pages - if err := tx.GetContext(ctx, &createdPage, pageSQL, pageArgs...); err != nil { - rollback(fmt.Sprintf("Failed to insert rol_pages at index %d", idx), err, http.StatusInternalServerError) - return - } - - // Insert components - createdComponents := make([]pagesModels.Rol_component, 0) - if req.Components != nil { - for _, compReq := range req.Components { - compInsert := queryUtils.InsertData{ - Columns: []string{"name", "description", "directory", "sort", "active", "fk_rol_pages_id"}, - Values: []interface{}{compReq.Name, compReq.Description, compReq.Directory, compReq.Sort, compReq.Active, createdPage.ID}, - } - compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} - compSQL, compArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", compInsert, compReturning...) - if err != nil { - rollback("Failed to build insert query for component", err, http.StatusInternalServerError) - return - } - var createdComp pagesModels.Rol_component - if err := tx.GetContext(ctx, &createdComp, compSQL, compArgs...); err != nil { - rollback("Failed to insert rol_component", err, http.StatusInternalServerError) - return - } - createdComponents = append(createdComponents, createdComp) - } - } - - // Insert permissions - createdPermissions := make([]pagesModels.Rol_permission, 0) - if req.Permissions != nil { - for _, permReq := range req.Permissions { - createNB := toNullBool(permReq.Create) - readNB := toNullBool(permReq.Read) - updateNB := toNullBool(permReq.Update) - deleteNB := toNullBool(permReq.Delete) - disableNB := toNullBool(permReq.Disable) - activeNB := toNullBool(permReq.Active) - - roles := toPQStringArray(permReq.RoleKeycloak) - groups := toPQStringArray(permReq.GroupKeycloak) - - permInsert := queryUtils.InsertData{ - Columns: []string{ - `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, - `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, - }, - Values: []interface{}{ - createNB, readNB, updateNB, deleteNB, disableNB, activeNB, - createdPage.ID, roles, groups, - }, - } - permReturning := []string{ - `"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, - `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, - } - permSQL, permArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_permission", permInsert, permReturning...) - if err != nil { - rollback("Failed to build insert query for permission", err, http.StatusInternalServerError) - return - } - var createdPerm pagesModels.Rol_permission - if err := tx.GetContext(ctx, &createdPerm, permSQL, permArgs...); err != nil { - rollback("Failed to insert rol_permission", err, http.StatusInternalServerError) - return - } - createdPermissions = append(createdPermissions, createdPerm) - } - } - - createdPage.Components = createdComponents - createdPage.Permissions = createdPermissions - createdPages = append(createdPages, createdPage) - } - - // Commit - if err := tx.Commit(); err != nil { - _ = tx.Rollback() - h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) + h.logAndRespondError(c, "Failed to create rol_pages", err, http.StatusInternalServerError) return } @@ -610,12 +474,6 @@ func (h *Rol_pagesHandler) CreateRol_pages(c *gin.Context) { // @Router /pages/{id} [put] func (h *Rol_pagesHandler) UpdateRol_pages(c *gin.Context) { id := c.Param("id") - // if id == "" { - // h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) - // return - // } - - // Validate ID is integer if _, err := strconv.Atoi(id); err != nil { h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) return @@ -627,7 +485,6 @@ func (h *Rol_pagesHandler) UpdateRol_pages(c *gin.Context) { return } - // Set ID from path parameter idInt, _ := strconv.Atoi(id) req.ID = &idInt @@ -637,312 +494,45 @@ func (h *Rol_pagesHandler) UpdateRol_pages(c *gin.Context) { } // Get old data for cache invalidation - var oldData pagesModels.Rol_pages - dbConn, err := h.db.GetSQLXDB("db_antrean") - if err == nil { - ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) - defer cancel() - - dynamicQuery := queryUtils.DynamicQuery{ - From: "role_access.rol_pages", - Aliases: "rp", - Fields: []queryUtils.SelectField{{Expression: "*"}}, - Filters: []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "rp.id", Operator: queryUtils.OpEqual, Value: id}, - }, - LogicOp: "AND", - }}, - Limit: 1, - Joins: []queryUtils.Join{ - { - Type: "LEFT", - Table: "role_access.rol_component", - Alias: "rc", - OnConditions: queryUtils.FilterGroup{ - Filters: []queryUtils.DynamicFilter{ - {Column: "rc.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, - }, - }, - }, - }, - } - - err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &oldData) - if err != nil { - logger.Error("Failed to fetch old data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) - } + oldData, err := h.getPageByID(id) + if err != nil { + logger.Error("Failed to fetch old data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) } - dbConn, err = h.db.GetSQLXDB("db_antrean") + 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() // Validate id must be unique, except for record with this id if req.ID != nil { - rule := validation.ValidationRule{ - TableName: "role_access.rol_pages", - UniqueColumns: []string{"id"}, - Conditions: []queryUtils.DynamicFilter{ - {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, - }, - ExcludeIDColumn: "id", - ExcludeIDValue: id, - } - - dataToValidate := map[string]interface{}{"id": *req.ID} - isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) - if err != nil { + if isDuplicate, err := h.validateUniqueID(ctx, dbConn, *req.ID, id); err != nil { h.logAndRespondError(c, "Failed to validate id", err, http.StatusInternalServerError) return - } - - if isDuplicate { + } else if isDuplicate { h.respondError(c, "id already exists", fmt.Errorf("duplicate id: %d", *req.ID), http.StatusConflict) return } } - columns := make([]string, 0, 7) - values := make([]interface{}, 0, 7) - - if req.Name != nil { - columns = append(columns, "name") - values = append(values, *req.Name) - } - if req.Icon != nil { - columns = append(columns, "icon") - values = append(values, *req.Icon) - } - if req.Url != nil { - columns = append(columns, "url") - values = append(values, *req.Url) - } - if req.Level != nil { - columns = append(columns, "level") - values = append(values, *req.Level) - } - if req.Sort != nil { - columns = append(columns, "sort") - values = append(values, *req.Sort) - } - - 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", "icon", "url", "level", "sort", "parent", "active"} - - // Start transaction for update including components and permissions - tx, err := dbConn.BeginTxx(ctx, nil) + // Execute update in a transaction + updatedPage, err := h.executeUpdateTransaction(ctx, dbConn, req) if err != nil { - h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) - return - } - rollback := func(errMsg string, err error, code int) { - _ = tx.Rollback() - h.logAndRespondError(c, errMsg, err, code) - } - - var updatedPage pagesModels.Rol_pages - - // If there are no page-level columns to update, skip UPDATE and SELECT existing row - if len(updateData.Columns) == 0 { - if err := tx.GetContext(ctx, &updatedPage, "SELECT id, name, icon, url, level, sort, parent, active FROM role_access.rol_pages WHERE id=$1", *req.ID); err != nil { - _ = tx.Rollback() - if err == sql.ErrNoRows { - h.respondError(c, "Rol_pages not found", err, http.StatusNotFound) - return - } - h.logAndRespondError(c, "Failed to fetch existing rol_pages", err, http.StatusInternalServerError) - return - } - } else { - // Update page row and get returning - sqlQuery, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", updateData, filters, returningCols...) - if err != nil { - rollback("Failed to build update query", err, http.StatusInternalServerError) - return - } - - if err := tx.GetContext(ctx, &updatedPage, sqlQuery, args...); err != nil { - _ = tx.Rollback() - if err == sql.ErrNoRows || err.Error() == "sql: no rows in result set" { - h.respondError(c, "Rol_pages not found", err, http.StatusNotFound) - return - } - h.logAndRespondError(c, "Failed to update rol_pages", err, http.StatusInternalServerError) - return - } - } - - // Handle components (insert or update) if provided - updatedComponents := make([]pagesModels.Rol_component, 0) - if req.Components != nil { - for _, compReq := range req.Components { - // If ID present => update, else insert - if compReq.ID != nil && *compReq.ID > 0 { - compCols := make([]string, 0) - compVals := make([]interface{}, 0) - if compReq.Name != nil { - compCols = append(compCols, "name") - compVals = append(compVals, *compReq.Name) - } - if compReq.Description != nil { - compCols = append(compCols, "description") - compVals = append(compVals, *compReq.Description) - } - if compReq.Directory != nil { - compCols = append(compCols, "directory") - compVals = append(compVals, *compReq.Directory) - } - if compReq.Sort != nil { - compCols = append(compCols, "sort") - compVals = append(compVals, *compReq.Sort) - } - if compReq.Active != nil { - compCols = append(compCols, "active") - compVals = append(compVals, *compReq.Active) - } - if len(compCols) > 0 { - compUpdate := queryUtils.UpdateData{Columns: compCols, Values: compVals} - compFilters := []queryUtils.FilterGroup{{Filters: []queryUtils.DynamicFilter{ - {Column: "id", Operator: queryUtils.OpEqual, Value: *compReq.ID}, - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: updatedPage.ID}, - }, - - LogicOp: "AND", - }} - compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} - compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters, compReturning...) - if err != nil { - rollback("Failed to build component update query", err, http.StatusInternalServerError) - return - } - var updatedComp pagesModels.Rol_component - if err := tx.GetContext(ctx, &updatedComp, compSQL, compArgs...); err != nil { - rollback("Failed to update rol_component", err, http.StatusInternalServerError) - return - } - updatedComponents = append(updatedComponents, updatedComp) - } - } else { - // Do NOT insert new component on update. - // If client wants to add a new component, use Create endpoint. - rollback("Component not found or missing id", fmt.Errorf("component must include id to be updated; insert not allowed in UpdateRol_pages"), http.StatusNotFound) - return - } - } - } - - // Handle permissions (insert or update) if provided - updatedPermissions := make([]pagesModels.Rol_permission, 0) - if req.Permissions != nil { - for _, permReq := range req.Permissions { - createNB := toNullBool(permReq.Create) - readNB := toNullBool(permReq.Read) - updateNB := toNullBool(permReq.Update) - deleteNB := toNullBool(permReq.Delete) - disableNB := toNullBool(permReq.Disable) - activeNB := toNullBool(permReq.Active) - roles := toPQStringArray(permReq.RoleKeycloak) - groups := toPQStringArray(permReq.GroupKeycloak) - - if permReq.ID != nil && *permReq.ID > 0 { - permCols := make([]string, 0) - permVals := make([]interface{}, 0) - - if permReq.Create != nil { - permCols = append(permCols, `"create"`) - permVals = append(permVals, createNB) - } - if permReq.Read != nil { - permCols = append(permCols, `"read"`) - permVals = append(permVals, readNB) - } - if permReq.Update != nil { - permCols = append(permCols, `"update"`) - permVals = append(permVals, updateNB) - } - if permReq.Delete != nil { - permCols = append(permCols, `"delete"`) - permVals = append(permVals, deleteNB) - } - if permReq.Disable != nil { - permCols = append(permCols, `"disable"`) - permVals = append(permVals, disableNB) - } - if permReq.Active != nil { - permCols = append(permCols, `"active"`) - permVals = append(permVals, activeNB) - } - if permReq.RoleKeycloak != nil { - permCols = append(permCols, `"role_keycloak"`) - permVals = append(permVals, roles) - } - if permReq.GroupKeycloak != nil { - permCols = append(permCols, `"group_keycloak"`) - permVals = append(permVals, groups) - } - if len(permCols) > 0 { - permUpdate := queryUtils.UpdateData{Columns: permCols, Values: permVals} - permFilters := []queryUtils.FilterGroup{{Filters: []queryUtils.DynamicFilter{ - {Column: "id", Operator: queryUtils.OpEqual, Value: *permReq.ID}, - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: updatedPage.ID}, - }, - LogicOp: "AND", - }} - permReturning := []string{`"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`} - permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters, permReturning...) - if err != nil { - rollback("Failed to build permission update query", err, http.StatusInternalServerError) - return - } - var updatedPerm pagesModels.Rol_permission - if err := tx.GetContext(ctx, &updatedPerm, permSQL, permArgs...); err != nil { - rollback("Failed to update rol_permission", err, http.StatusInternalServerError) - return - } - updatedPermissions = append(updatedPermissions, updatedPerm) - } else { - // Do NOT insert new permission on update. - // If client wants to add a new permission, use Create endpoint. - rollback("Permission not found or missing id", fmt.Errorf("permission must include id to be updated; insert not allowed in Update endpoint"), http.StatusNotFound) - return - } - } - } - } - - // Commit transaction - if err := tx.Commit(); err != nil { - _ = tx.Rollback() - h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) + h.logAndRespondError(c, "Failed to update rol_pages", err, http.StatusInternalServerError) return } // Invalidate cache - cacheKey := fmt.Sprintf("rol_pages:id:%s", id) - h.cache.Delete(cacheKey) + h.cache.Delete(fmt.Sprintf("rol_pages:id:%s", id)) if oldData.ID != 0 { h.invalidateRelatedCache() } - // Attach related rows - updatedPage.Components = updatedComponents - updatedPage.Permissions = updatedPermissions - - response := pagesModels.PagesUpdateResponse{Message: "Rol_pages berhasil diperbarui", Data: &updatedPage} + response := pagesModels.PagesUpdateResponse{Message: "Rol_pages berhasil diperbarui", Data: updatedPage} c.JSON(http.StatusOK, response) } @@ -960,12 +550,6 @@ func (h *Rol_pagesHandler) UpdateRol_pages(c *gin.Context) { // @Router /pages/{id} [delete] func (h *Rol_pagesHandler) DeleteRol_pages(c *gin.Context) { id := c.Param("id") - // if id == "" { - // h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) - // return - // } - - // Validate ID is integer if _, err := strconv.Atoi(id); err != nil { h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) return @@ -976,66 +560,635 @@ func (h *Rol_pagesHandler) DeleteRol_pages(c *gin.Context) { ComponentIDs []int64 `json:"component_ids"` PermissionIDs []int64 `json:"permission_ids"` } - // ignore error so empty body is allowed - _ = c.ShouldBindJSON(&delReq) + _ = c.ShouldBindJSON(&delReq) // ignore error so empty body is allowed // Get data for cache invalidation - var dataToDelete pagesModels.Rol_pages - dbConn, err := h.db.GetSQLXDB("db_antrean") - if err == nil { - ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) - defer cancel() - - dynamicQuery := queryUtils.DynamicQuery{ - From: "role_access.rol_pages", - Aliases: "rp", - Fields: []queryUtils.SelectField{{Expression: "*"}}, - Filters: []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "id", Operator: queryUtils.OpEqual, Value: id}, - }, - LogicOp: "AND", - }}, - Limit: 1, - Joins: []queryUtils.Join{ - { - Type: "LEFT", - Table: "role_access.rol_component", - Alias: "rc", - OnConditions: queryUtils.FilterGroup{ - Filters: []queryUtils.DynamicFilter{ - {Column: "rc.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, - }, - }, - }, - }, - } - - err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &dataToDelete) - if err != nil { - logger.Error("Failed to fetch data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) - } + dataToDelete, err := h.getPageByID(id) + if err != nil { + logger.Error("Failed to fetch data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) } - dbConn, err = h.db.GetSQLXDB("db_antrean") + 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() - // Start transaction - tx, err := dbConn.BeginTxx(ctx, nil) + // Execute delete in a transaction + err = h.executeDeleteTransaction(ctx, dbConn, id, delReq) if err != nil { - h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) + h.logAndRespondError(c, "Failed to delete rol_pages", err, http.StatusInternalServerError) return } - rollback := func(msg string, err error, code int) { - _ = tx.Rollback() - h.logAndRespondError(c, msg, err, code) + + // Invalidate cache + h.cache.Delete(fmt.Sprintf("rol_pages:id:%s", id)) + if dataToDelete.ID != 0 { + h.invalidateRelatedCache() } + response := pagesModels.PagesDeleteResponse{Message: "Rol_pages berhasil dihapus", ID: id} + c.JSON(http.StatusOK, response) +} + +// UpdateRol_pagesBulk godoc +// @Summary Bulk update pages +// @Description Update multiple pages in a single request. Each object must include "id". Use /pages/bulk [put]. +// @Tags Pages +// @Accept json +// @Produce json +// @Param request body []pagesModels.PagesUpdateRequest true "Array of rol_pages update requests" +// @Success 200 {object} []pagesModels.PagesUpdateResponse "Updated pages" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /pages/bulk [put] +func (h *Rol_pagesHandler) UpdateRol_pagesBulk(c *gin.Context) { + // Parse request body (supports both single object and array) + reqs, err := h.parseUpdateRequest(c) + if err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + if len(reqs) == 0 { + h.respondError(c, "No update objects provided", fmt.Errorf("no objects"), http.StatusBadRequest) + return + } + + // Ensure every object has numeric ID and validate + for i := range reqs { + if reqs[i].ID == nil { + h.respondError(c, fmt.Sprintf("Missing id for update at index %d", i), fmt.Errorf("id required"), http.StatusBadRequest) + return + } + if _, err := strconv.Atoi(fmt.Sprintf("%d", *reqs[i].ID)); err != nil { + h.respondError(c, fmt.Sprintf("Invalid id at index %d", i), err, http.StatusBadRequest) + return + } + if err := validate.Struct(&reqs[i]); err != nil { + h.respondError(c, fmt.Sprintf("Validation failed at index %d", i), 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(), 60*time.Second) + defer cancel() + + // Execute bulk update in a transaction + updatedPages, err := h.executeBulkUpdateTransaction(ctx, dbConn, reqs) + if err != nil { + h.logAndRespondError(c, "Failed to update rol_pages", err, http.StatusInternalServerError) + return + } + + // Invalidate cache + for _, p := range updatedPages { + h.cache.Delete(fmt.Sprintf("rol_pages:id:%d", p.ID)) + } + h.invalidateRelatedCache() + + // Respond single or multiple + if len(updatedPages) == 1 { + resp := pagesModels.PagesUpdateResponse{Message: "Rol_pages berhasil diperbarui", Data: &updatedPages[0]} + c.JSON(http.StatusOK, resp) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Multiple rol_pages berhasil diperbarui", "data": updatedPages}) +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +// parsePagination parses pagination parameters from the request +func (h *Rol_pagesHandler) parsePagination(c *gin.Context, query *queryUtils.DynamicQuery) { + 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 + } +} + +// parseCreateRequest parses the create request body (supports both single object and array) +func (h *Rol_pagesHandler) parseCreateRequest(c *gin.Context) ([]pagesModels.PagesCreateRequest, error) { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + return nil, err + } + + trimmed := bytes.TrimSpace(body) + if len(trimmed) == 0 { + return nil, fmt.Errorf("empty body") + } + + var reqs []pagesModels.PagesCreateRequest + if trimmed[0] == '[' { + if err := json.Unmarshal(trimmed, &reqs); err != nil { + return nil, fmt.Errorf("invalid request body (array): %w", err) + } + } else { + var single pagesModels.PagesCreateRequest + if err := json.Unmarshal(trimmed, &single); err != nil { + return nil, fmt.Errorf("invalid request body (object): %w", err) + } + reqs = append(reqs, single) + } + + return reqs, nil +} + +// parseUpdateRequest parses the update request body (supports both single object and array) +func (h *Rol_pagesHandler) parseUpdateRequest(c *gin.Context) ([]pagesModels.PagesUpdateRequest, error) { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + return nil, err + } + + trimmed := bytes.TrimSpace(body) + if len(trimmed) == 0 { + return nil, fmt.Errorf("empty body") + } + + var reqs []pagesModels.PagesUpdateRequest + if trimmed[0] == '[' { + if err := json.Unmarshal(trimmed, &reqs); err != nil { + return nil, fmt.Errorf("invalid request body (array): %w", err) + } + } else { + var single pagesModels.PagesUpdateRequest + if err := json.Unmarshal(trimmed, &single); err != nil { + return nil, fmt.Errorf("invalid request body (object): %w", err) + } + reqs = append(reqs, single) + } + + return reqs, nil +} + +// validateUniqueID validates that an ID is unique (excluding the current record if excludeID is provided) +func (h *Rol_pagesHandler) validateUniqueID(ctx context.Context, dbConn *sqlx.DB, id int, excludeID string) (bool, error) { + rule := validation.ValidationRule{ + TableName: "role_access.rol_pages", + UniqueColumns: []string{"id"}, + Conditions: []queryUtils.DynamicFilter{ + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + } + + if excludeID != "" { + rule.ExcludeIDColumn = "id" + rule.ExcludeIDValue = excludeID + } + + dataToValidate := map[string]interface{}{"id": id} + return h.validator.Validate(ctx, dbConn, rule, dataToValidate) +} + +// getPageByID retrieves a page by ID for cache invalidation +func (h *Rol_pagesHandler) getPageByID(id string) (pagesModels.Rol_pages, error) { + var data pagesModels.Rol_pages + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + return data, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + dynamicQuery := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{{Expression: "*"}}, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rp.id", Operator: queryUtils.OpEqual, Value: id}, + }, + LogicOp: "AND", + }}, + Limit: 1, + } + + err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &data) + return data, err +} + +// executeCreateTransaction executes the create operation in a transaction +func (h *Rol_pagesHandler) executeCreateTransaction(ctx context.Context, dbConn *sqlx.DB, reqs []pagesModels.PagesCreateRequest) ([]pagesModels.Rol_pages, error) { + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + createdPages := make([]pagesModels.Rol_pages, 0, len(reqs)) + + for idx, req := range reqs { + // Insert page + pageInsert := queryUtils.InsertData{ + Columns: []string{"name", "icon", "url", "level", "sort", "parent", "active"}, + Values: []interface{}{req.Name, req.Icon, req.Url, req.Level, req.Sort, req.Parent, req.Active}, + } + pageReturning := []string{"id", "name", "icon", "url", "level", "sort", "parent", "active"} + pageSQL, pageArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_pages", pageInsert, pageReturning...) + if err != nil { + return nil, fmt.Errorf("failed to build insert query for page at index %d: %w", idx, err) + } + + var createdPage pagesModels.Rol_pages + if err := tx.GetContext(ctx, &createdPage, pageSQL, pageArgs...); err != nil { + return nil, fmt.Errorf("failed to insert rol_pages at index %d: %w", idx, err) + } + + // Insert components + var createdComponents []pagesModels.Rol_component + if req.Components != nil { + createdComponents, err = h.insertComponents(ctx, tx, req.Components, createdPage.ID) + if err != nil { + return nil, fmt.Errorf("failed to insert components at index %d: %w", idx, err) + } + } + + // Insert permissions + var createdPermissions []pagesModels.Rol_permission + if req.Permissions != nil { + createdPermissions, err = h.insertPermissions(ctx, tx, req.Permissions, createdPage.ID) + if err != nil { + return nil, fmt.Errorf("failed to insert permissions at index %d: %w", idx, err) + } + } + + createdPage.Components = createdComponents + createdPage.Permissions = createdPermissions + createdPages = append(createdPages, createdPage) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return createdPages, nil +} + +// insertComponents inserts components for a page +func (h *Rol_pagesHandler) insertComponents(ctx context.Context, tx *sqlx.Tx, components []pagesModels.ComponentCreateRequest, pageID int64) ([]pagesModels.Rol_component, error) { + createdComponents := make([]pagesModels.Rol_component, 0, len(components)) + + for _, compReq := range components { + compInsert := queryUtils.InsertData{ + Columns: []string{"name", "description", "directory", "sort", "active", "fk_rol_pages_id"}, + Values: []interface{}{compReq.Name, compReq.Description, compReq.Directory, compReq.Sort, compReq.Active, pageID}, + } + compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} + compSQL, compArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", compInsert, compReturning...) + if err != nil { + return nil, fmt.Errorf("failed to build insert query for component: %w", err) + } + + var createdComp pagesModels.Rol_component + if err := tx.GetContext(ctx, &createdComp, compSQL, compArgs...); err != nil { + return nil, fmt.Errorf("failed to insert rol_component: %w", err) + } + + createdComponents = append(createdComponents, createdComp) + } + + return createdComponents, nil +} + +// insertPermissions inserts permissions for a page +func (h *Rol_pagesHandler) insertPermissions(ctx context.Context, tx *sqlx.Tx, permissions []pagesModels.PermissionCreateRequest, pageID int64) ([]pagesModels.Rol_permission, error) { + createdPermissions := make([]pagesModels.Rol_permission, 0, len(permissions)) + + for _, permReq := range permissions { + permInsert := queryUtils.InsertData{ + Columns: []string{ + `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, + `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, + }, + Values: []interface{}{ + toNullBool(permReq.Create), toNullBool(permReq.Read), toNullBool(permReq.Update), + toNullBool(permReq.Delete), toNullBool(permReq.Disable), toNullBool(permReq.Active), + pageID, toPQStringArray(permReq.RoleKeycloak), toPQStringArray(permReq.GroupKeycloak), + }, + } + permReturning := []string{ + `"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, + `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, + } + permSQL, permArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_permission", permInsert, permReturning...) + if err != nil { + return nil, fmt.Errorf("failed to build insert query for permission: %w", err) + } + + var createdPerm pagesModels.Rol_permission + if err := tx.GetContext(ctx, &createdPerm, permSQL, permArgs...); err != nil { + return nil, fmt.Errorf("failed to insert rol_permission: %w", err) + } + + createdPermissions = append(createdPermissions, createdPerm) + } + + return createdPermissions, nil +} + +// executeUpdateTransaction executes the update operation in a transaction +func (h *Rol_pagesHandler) executeUpdateTransaction(ctx context.Context, dbConn *sqlx.DB, req pagesModels.PagesUpdateRequest) (*pagesModels.Rol_pages, error) { + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + // Build update columns from provided fields + columns, values := h.buildUpdateColumns(req) + + var updatedPage pagesModels.Rol_pages + + if len(columns) == 0 { + // No page-level columns to update: select existing row + updatedPage, err = h.selectPageByID(ctx, dbConn, *req.ID) + if err != nil { + return nil, fmt.Errorf("failed to fetch existing rol_pages: %w", err) + } + } else { + // Update page row and get returning + 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", "icon", "url", "level", "sort", "parent", "active"} + + sqlQuery, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", updateData, filters, returningCols...) + if err != nil { + return nil, fmt.Errorf("failed to build update query: %w", err) + } + + if err := tx.GetContext(ctx, &updatedPage, sqlQuery, args...); err != nil { + if err == sql.ErrNoRows || err.Error() == "sql: no rows in result set" { + return nil, fmt.Errorf("rol_pages not found: %w", err) + } + return nil, fmt.Errorf("failed to update rol_pages: %w", err) + } + } + + // Handle components (update only if ID provided) + var updatedComponents []pagesModels.Rol_component + if req.Components != nil { + updatedComponents, err = h.updateComponents(ctx, tx, req.Components, updatedPage.ID) + if err != nil { + return nil, fmt.Errorf("failed to update components: %w", err) + } + } + + // Handle permissions (update only if ID provided) + var updatedPermissions []pagesModels.Rol_permission + if req.Permissions != nil { + updatedPermissions, err = h.updatePermissions(ctx, tx, req.Permissions, updatedPage.ID) + if err != nil { + return nil, fmt.Errorf("failed to update permissions: %w", err) + } + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + // Attach related rows + updatedPage.Components = updatedComponents + updatedPage.Permissions = updatedPermissions + + return &updatedPage, nil +} + +// buildUpdateColumns builds update columns and values from the request +func (h *Rol_pagesHandler) buildUpdateColumns(req pagesModels.PagesUpdateRequest) ([]string, []interface{}) { + return buildUpdateColumnsFromStruct(req, pageFieldToColumn) +} + +// selectPageByID selects a page by ID +func (h *Rol_pagesHandler) selectPageByID(ctx context.Context, dbConn *sqlx.DB, id int) (pagesModels.Rol_pages, error) { + var updatedPage pagesModels.Rol_pages + dq := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{ + {Expression: "rp.id", Alias: "id"}, + {Expression: "rp.name", Alias: "name"}, + {Expression: "rp.icon", Alias: "icon"}, + {Expression: "rp.url", Alias: "url"}, + {Expression: "rp.level", Alias: "level"}, + {Expression: "rp.sort", Alias: "sort"}, + {Expression: "rp.parent", Alias: "parent"}, + {Expression: "rp.active", Alias: "active"}, + }, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rp.id", Operator: queryUtils.OpEqual, Value: id}, + }, + LogicOp: "AND", + }}, + Limit: 1, + } + + err := h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dq, &updatedPage) + if err != nil { + return updatedPage, fmt.Errorf("failed to fetch rol_pages: %w", err) + } + + return updatedPage, nil +} + +// updateComponents updates components for a page +func (h *Rol_pagesHandler) updateComponents(ctx context.Context, tx *sqlx.Tx, components []pagesModels.ComponentUpdateRequest, pageID int64) ([]pagesModels.Rol_component, error) { + updatedComponents := make([]pagesModels.Rol_component, 0, len(components)) + + for _, compReq := range components { + if compReq.ID == nil || *compReq.ID <= 0 { + return nil, fmt.Errorf("component must include id to be updated; insert not allowed in UpdateRol_pages") + } + + compCols, compVals := h.buildComponentUpdateColumns(compReq) + if len(compCols) == 0 { + continue // No columns to update + } + + compUpdate := queryUtils.UpdateData{Columns: compCols, Values: compVals} + compFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: *compReq.ID}, + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: pageID}, + }, + LogicOp: "AND", + }} + compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} + compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters, compReturning...) + if err != nil { + return nil, fmt.Errorf("failed to build component update query: %w", err) + } + + var updatedComp pagesModels.Rol_component + if err := tx.GetContext(ctx, &updatedComp, compSQL, compArgs...); err != nil { + return nil, fmt.Errorf("failed to update rol_component: %w", err) + } + + updatedComponents = append(updatedComponents, updatedComp) + } + + return updatedComponents, nil +} + +// buildComponentUpdateColumns builds update columns and values for a component +func (h *Rol_pagesHandler) buildComponentUpdateColumns(compReq pagesModels.ComponentUpdateRequest) ([]string, []interface{}) { + return buildUpdateColumnsFromStruct(compReq, componentFieldToColumn) +} + +// updatePermissions updates permissions for a page +func (h *Rol_pagesHandler) updatePermissions(ctx context.Context, tx *sqlx.Tx, permissions []pagesModels.PermissionUpdateRequest, pageID int64) ([]pagesModels.Rol_permission, error) { + updatedPermissions := make([]pagesModels.Rol_permission, 0, len(permissions)) + + for _, permReq := range permissions { + if permReq.ID == nil || *permReq.ID <= 0 { + return nil, fmt.Errorf("permission must include id to be updated; insert not allowed in Update endpoint") + } + + permCols, permVals := h.buildPermissionUpdateColumns(permReq) + if len(permCols) == 0 { + continue // No columns to update + } + + permUpdate := queryUtils.UpdateData{Columns: permCols, Values: permVals} + permFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: *permReq.ID}, + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: pageID}, + }, + LogicOp: "AND", + }} + permReturning := []string{`"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`} + permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters, permReturning...) + if err != nil { + return nil, fmt.Errorf("failed to build permission update query: %w", err) + } + + var updatedPerm pagesModels.Rol_permission + if err := tx.GetContext(ctx, &updatedPerm, permSQL, permArgs...); err != nil { + return nil, fmt.Errorf("failed to update rol_permission: %w", err) + } + + updatedPermissions = append(updatedPermissions, updatedPerm) + } + + return updatedPermissions, nil +} + +// buildPermissionUpdateColumns builds update columns and values for a permission +func (h *Rol_pagesHandler) buildPermissionUpdateColumns(permReq pagesModels.PermissionUpdateRequest) ([]string, []interface{}) { + // Define the mapping and the logic to get the transformed value + type fieldInfo struct { + column string + getValue func() (interface{}, bool) // Returns (value, shouldInclude) + } + + mappings := []fieldInfo{ + {`"create"`, func() (interface{}, bool) { + if permReq.Create == nil { + return nil, false + } + return toNullBool(permReq.Create), true + }}, + {`"read"`, func() (interface{}, bool) { + if permReq.Read == nil { + return nil, false + } + return toNullBool(permReq.Read), true + }}, + {`"update"`, func() (interface{}, bool) { + if permReq.Update == nil { + return nil, false + } + return toNullBool(permReq.Update), true + }}, + {`"delete"`, func() (interface{}, bool) { + if permReq.Delete == nil { + return nil, false + } + return toNullBool(permReq.Delete), true + }}, + {`"disable"`, func() (interface{}, bool) { + if permReq.Disable == nil { + return nil, false + } + return toNullBool(permReq.Disable), true + }}, + {`"active"`, func() (interface{}, bool) { + if permReq.Active == nil { + return nil, false + } + return toNullBool(permReq.Active), true + }}, + {`"role_keycloak"`, func() (interface{}, bool) { + if permReq.RoleKeycloak == nil { + return nil, false + } + return toPQStringArray(permReq.RoleKeycloak), true + }}, + {`"group_keycloak"`, func() (interface{}, bool) { + if permReq.GroupKeycloak == nil { + return nil, false + } + return toPQStringArray(permReq.GroupKeycloak), true + }}, + } + + columns := make([]string, 0, len(mappings)) + values := make([]interface{}, 0, len(mappings)) + + for _, m := range mappings { + if val, ok := m.getValue(); ok { + columns = append(columns, m.column) + values = append(values, val) + } + } + + return columns, values +} + +// executeDeleteTransaction executes the delete operation in a transaction +func (h *Rol_pagesHandler) executeDeleteTransaction(ctx context.Context, dbConn *sqlx.DB, id string, delReq struct { + ComponentIDs []int64 `json:"component_ids"` + PermissionIDs []int64 `json:"permission_ids"` +}) error { + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + // Soft-delete page (active = false) only if currently active != false pageUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} pageFilters := []queryUtils.FilterGroup{{ @@ -1047,140 +1200,182 @@ func (h *Rol_pagesHandler) DeleteRol_pages(c *gin.Context) { }} pageSQL, pageArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", pageUpdate, pageFilters) if err != nil { - rollback("Failed to build delete query for page", err, http.StatusInternalServerError) - return + return fmt.Errorf("failed to build delete query for page: %w", err) } + res, err := tx.ExecContext(ctx, pageSQL, pageArgs...) if err != nil { - rollback("Failed to execute delete for page", err, http.StatusInternalServerError) - return + return fmt.Errorf("failed to execute delete for page: %w", err) } + ra, err := res.RowsAffected() if err != nil { - rollback("Failed to get affected rows for page delete", err, http.StatusInternalServerError) - return + return fmt.Errorf("failed to get affected rows for page delete: %w", err) } if ra == 0 { - _ = tx.Rollback() - h.respondError(c, "Rol_pages not found", sql.ErrNoRows, http.StatusNotFound) - return + return fmt.Errorf("rol_pages not found: %w", sql.ErrNoRows) } // Soft-delete related components - // If client provided specific component IDs -> delete only those; otherwise delete all components for the page. - if len(delReq.ComponentIDs) > 0 { - // Use query builder to update only specified component IDs that belong to this page - compUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} - compFilters := []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, - {Column: "id", Operator: queryUtils.OpIn, Value: delReq.ComponentIDs}, - {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, - }, - LogicOp: "AND", - }} - compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters) - if err != nil { - rollback("Failed to build component delete query", err, http.StatusInternalServerError) - return - } - if _, err := tx.ExecContext(ctx, compSQL, compArgs...); err != nil { - rollback("Failed to execute delete for components", err, http.StatusInternalServerError) - return - } - } else { - compUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} - compFilters := []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, - {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, - }, - LogicOp: "AND", - }} - compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters) - if err != nil { - rollback("Failed to build delete query for components", err, http.StatusInternalServerError) - return - } - if _, err := tx.ExecContext(ctx, compSQL, compArgs...); err != nil { - rollback("Failed to execute delete for components", err, http.StatusInternalServerError) - return - } + if err := h.deleteRelatedComponents(ctx, tx, id, delReq.ComponentIDs); err != nil { + return fmt.Errorf("failed to delete components: %w", err) } // Soft-delete related permissions - // If client provided specific permission IDs -> delete only those; otherwise delete all permissions for the page. - if len(delReq.PermissionIDs) > 0 { - permUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} - permFilters := []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, - {Column: "id", Operator: queryUtils.OpIn, Value: delReq.PermissionIDs}, - {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, - }, - LogicOp: "AND", - }} - permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters) - if err != nil { - rollback("Failed to build permission delete query", err, http.StatusInternalServerError) - return - } - if _, err := tx.ExecContext(ctx, permSQL, permArgs...); err != nil { - rollback("Failed to execute delete for permissions", err, http.StatusInternalServerError) - return - } - } else { - permUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} - permFilters := []queryUtils.FilterGroup{{ - Filters: []queryUtils.DynamicFilter{ - {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, - {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, - }, - LogicOp: "AND", - }} - permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters) - if err != nil { - rollback("Failed to build delete query for permissions", err, http.StatusInternalServerError) - return - } - if _, err := tx.ExecContext(ctx, permSQL, permArgs...); err != nil { - rollback("Failed to execute delete for permissions", err, http.StatusInternalServerError) - return - } + if err := h.deleteRelatedPermissions(ctx, tx, id, delReq.PermissionIDs); err != nil { + return fmt.Errorf("failed to delete permissions: %w", err) } - // Commit if err := tx.Commit(); err != nil { - _ = tx.Rollback() - h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) - return + return fmt.Errorf("failed to commit transaction: %w", err) } - // Invalidate cache - cacheKey := fmt.Sprintf("rol_pages:id:%s", id) - h.cache.Delete(cacheKey) - if dataToDelete.ID != 0 { - h.invalidateRelatedCache() - } - - response := pagesModels.PagesDeleteResponse{Message: "Rol_pages berhasil dihapus", ID: id} - c.JSON(http.StatusOK, response) + return nil } -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= +// deleteRelatedComponents deletes related components for a page +func (h *Rol_pagesHandler) deleteRelatedComponents(ctx context.Context, tx *sqlx.Tx, id string, componentIDs []int64) error { + compUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + var compFilters []queryUtils.FilterGroup + if len(componentIDs) > 0 { + // Delete only specified component IDs that belong to this page + compFilters = []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "id", Operator: queryUtils.OpIn, Value: componentIDs}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + } else { + // Delete all components for the page + compFilters = []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + } + + compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters) + if err != nil { + return fmt.Errorf("failed to build component delete query: %w", err) + } + + if _, err := tx.ExecContext(ctx, compSQL, compArgs...); err != nil { + return fmt.Errorf("failed to execute delete for components: %w", err) + } + + return nil +} + +// deleteRelatedPermissions deletes related permissions for a page +func (h *Rol_pagesHandler) deleteRelatedPermissions(ctx context.Context, tx *sqlx.Tx, id string, permissionIDs []int64) error { + permUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + + var permFilters []queryUtils.FilterGroup + if len(permissionIDs) > 0 { + // Delete only specified permission IDs that belong to this page + permFilters = []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "id", Operator: queryUtils.OpIn, Value: permissionIDs}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + } else { + // Delete all permissions for the page + permFilters = []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + } + + permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters) + if err != nil { + return fmt.Errorf("failed to build permission delete query: %w", err) + } + + if _, err := tx.ExecContext(ctx, permSQL, permArgs...); err != nil { + return fmt.Errorf("failed to execute delete for permissions: %w", err) + } + + return nil +} + +// executeBulkUpdateTransaction executes the bulk update operation in a transaction +func (h *Rol_pagesHandler) executeBulkUpdateTransaction(ctx context.Context, dbConn *sqlx.DB, reqs []pagesModels.PagesUpdateRequest) ([]pagesModels.Rol_pages, error) { + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + updatedPages := make([]pagesModels.Rol_pages, 0, len(reqs)) + + for idx, req := range reqs { + // Validate uniqueness of id excluding itself + if isDuplicate, err := h.validateUniqueID(ctx, dbConn, *req.ID, fmt.Sprintf("%d", *req.ID)); err != nil { + return nil, fmt.Errorf("failed to validate id at index %d: %w", idx, err) + } else if isDuplicate { + return nil, fmt.Errorf("id already exists at index %d: %d", idx, *req.ID) + } + + // Build update columns from provided fields + columns, values := h.buildUpdateColumns(req) + 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", "icon", "url", "level", "sort", "parent", "active"} + + var updated pagesModels.Rol_pages + + if len(columns) == 0 { + // No page-level columns to update: select current row + updated, err = h.selectPageByID(ctx, dbConn, *req.ID) + if err != nil { + return nil, fmt.Errorf("failed to fetch rol_pages at index %d: %w", idx, err) + } + } else { + sqlQuery, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", updateData, filters, returningCols...) + if err != nil { + return nil, fmt.Errorf("failed to build update query at index %d: %w", idx, err) + } + if err := tx.GetContext(ctx, &updated, sqlQuery, args...); err != nil { + return nil, fmt.Errorf("failed to update rol_pages at index %d: %w", idx, err) + } + } + + updatedPages = append(updatedPages, updated) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return updatedPages, nil +} + +// processQueryResults processes the query results into a structured format func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) []pagesModels.Rol_pages { pagesMap := make(map[int64]*pagesModels.Rol_pages) - order := make([]int64, 0, len(results)) // preserve encounter order + order := make([]int64, 0, len(results)) for _, result := range results { - // === ADD THIS DEBUG LOG === - // logger.Info("Processing result row", map[string]interface{}{ - // "row_index": i, - // "data": result, // This will print the entire map for the first few rows - // }) pageID := getInt64(result, "id") page, exists := pagesMap[pageID] @@ -1201,18 +1396,10 @@ func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) order = append(order, pageID) } - componentID := getInt64(result, "component_id") - if componentID != 0 { - // Check if this component has already been added to this page's list - // A map is much faster for this check than iterating over a slice. - // We create a temporary map just for this check. - seenComponents := make(map[int64]struct{}) - for _, comp := range page.Components { - seenComponents[comp.ID] = struct{}{} - } - - if _, seen := seenComponents[componentID]; !seen { - component := pagesModels.Rol_component{ + // Process component + if componentID := getInt64(result, "component_id"); componentID != 0 { + if !h.hasComponent(page, componentID) { + page.Components = append(page.Components, pagesModels.Rol_component{ ID: componentID, FkRolPagesID: getInt64(result, "component_fk"), Name: getString(result, "component_name"), @@ -1220,21 +1407,14 @@ func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) Directory: getString(result, "component_directory"), Active: getBool(result, "component_active"), Sort: getInt64(result, "component_sort"), - } - page.Components = append(page.Components, component) + }) } } - permissionID := getInt64(result, "permission_id") - if permissionID != 0 { - // Check if this permission has already been added to this page's list - seenPermissions := make(map[int64]struct{}) - for _, perm := range page.Permissions { - seenPermissions[perm.ID] = struct{}{} - } - - if _, seen := seenPermissions[permissionID]; !seen { - permission := pagesModels.Rol_permission{ + // Process permission + if permissionID := getInt64(result, "permission_id"); permissionID != 0 { + if !h.hasPermission(page, permissionID) { + page.Permissions = append(page.Permissions, pagesModels.Rol_permission{ ID: permissionID, Create: getNullBool(result, "permission_create"), Read: getNullBool(result, "permission_read"), @@ -1245,15 +1425,13 @@ func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) FkRolPagesID: getNullableInt32(result, "permission_fk"), RoleKeycloak: getStringArray(result, "permission_role_keycloak"), GroupKeycloak: getStringArray(result, "permission_group_keycloak"), - } - page.Permissions = append(page.Permissions, permission) + }) } } - } rol_pages := make([]pagesModels.Rol_pages, 0, len(pagesMap)) - for _, id := range order { // append in DB result order + for _, id := range order { if p, ok := pagesMap[id]; ok { rol_pages = append(rol_pages, *p) } @@ -1262,136 +1440,27 @@ func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) return rol_pages } -// Helper functions -func getInt64(m map[string]interface{}, key string) int64 { - if val, ok := m[key]; ok { - if v, ok := val.(int64); ok { - return v - } - } - return 0 -} - -func getString(m map[string]interface{}, key string) string { - if val, ok := m[key]; ok { - if v, ok := val.(string); ok { - return v - } - } - return "" -} - -// getNullString safely extracts a models.NullString from a map[string]interface{} -func getNullString(m map[string]interface{}, key string) sql.NullString { - if val, ok := m[key]; ok { - if ns, ok := val.(sql.NullString); ok { // The DB driver still returns sql.NullString - return sql.NullString(ns) // Convert it to your custom type - } - if s, ok := val.(string); ok { - return sql.NullString{String: s, Valid: true} - } - } - return sql.NullString{Valid: false} -} - -func getStringArray(m map[string]interface{}, key string) pq.StringArray { - if val, ok := m[key]; ok { - // Case 1: The DB driver correctly returns pq.StringArray (ideal case) - if arr, ok := val.(pq.StringArray); ok { - // === CLEAN THE ARRAY ELEMENTS HERE === - cleanedArray := make([]string, 0, len(arr)) - for _, element := range arr { - // Trim whitespace, then remove surrounding braces and quotes - cleaned := strings.TrimSpace(element) - cleaned = strings.TrimPrefix(cleaned, "{") - cleaned = strings.TrimSuffix(cleaned, "}") - cleaned = strings.Trim(cleaned, `"`) - cleanedArray = append(cleanedArray, cleaned) - } - return pq.StringArray(cleanedArray) - } - - // Case 2: The DB driver returns a string because the column is TEXT - if str, ok := val.(string); ok { - // This case is less likely now, but we handle it. - parts := strings.Split(str, ",") - var stringSlice []string - for _, part := range parts { - if t := strings.TrimSpace(part); t != "" { - // Apply same cleaning logic - cleaned := strings.TrimPrefix(t, "{") - cleaned = strings.TrimSuffix(cleaned, "}") - cleaned = strings.Trim(cleaned, `"`) - stringSlice = append(stringSlice, cleaned) - } - } - return pq.StringArray(stringSlice) - } - } - // Return an empty, valid pq.StringArray if the key doesn't exist. - return pq.StringArray{} -} - -func getNullableInt32(m map[string]interface{}, key string) models.NullableInt32 { - if val, ok := m[key]; ok { - if v, ok := val.(models.NullableInt32); ok { - return v - } - } - return models.NullableInt32{} -} - -func getBool(m map[string]interface{}, key string) bool { - if val, ok := m[key]; ok { - if v, ok := val.(bool); ok { - return v +// hasComponent checks if a component already exists in the page +func (h *Rol_pagesHandler) hasComponent(page *pagesModels.Rol_pages, componentID int64) bool { + for _, comp := range page.Components { + if comp.ID == componentID { + return true } } return false } -// getNullBool safely extracts a sql.NullBool from a map[string]interface{} -func getNullBool(m map[string]interface{}, key string) sql.NullBool { - if val, ok := m[key]; ok { - // Check if the value is already of type sql.NullBool - if nb, ok := val.(sql.NullBool); ok { - return nb - } - // Also handle the case where it's a plain bool, for flexibility - if b, ok := val.(bool); ok { - return sql.NullBool{Bool: b, Valid: true} +// hasPermission checks if a permission already exists in the page +func (h *Rol_pagesHandler) hasPermission(page *pagesModels.Rol_pages, permissionID int64) bool { + for _, perm := range page.Permissions { + if perm.ID == permissionID { + return true } } - // Return an invalid NullBool if the key doesn't exist or the type is wrong - return sql.NullBool{Valid: false} -} - -// helper to split comma-separated query params and trim spaces -func parseCSVParam(s string) []string { - parts := strings.Split(s, ",") - out := make([]string, 0, len(parts)) - for _, p := range parts { - if t := strings.TrimSpace(p); t != "" { - out = append(out, t) - } - } - return out -} - -func toNullBool(p *bool) sql.NullBool { - if p == nil { - return sql.NullBool{Valid: false} - } - return sql.NullBool{Bool: *p, Valid: true} -} - -func toPQStringArray(p *[]string) pq.StringArray { - if p == nil || len(*p) == 0 { - return pq.StringArray{} - } - return pq.StringArray(*p) + return false } +// getTotalCount gets the total count of records matching the query func (h *Rol_pagesHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { countQuery := queryUtils.DynamicQuery{ From: query.From, @@ -1408,6 +1477,7 @@ func (h *Rol_pagesHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, q return int(count), nil } +// invalidateRelatedCache invalidates all related cache entries func (h *Rol_pagesHandler) invalidateRelatedCache() { h.cache.DeleteByPrefix("rol_pages:search:") h.cache.DeleteByPrefix("rol_pages:dynamic:") @@ -1415,6 +1485,7 @@ func (h *Rol_pagesHandler) invalidateRelatedCache() { h.cache.DeleteByPrefix("rol_pages:id:") } +// getAggregateData gets aggregate data for the pages func (h *Rol_pagesHandler) getAggregateData(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), @@ -1554,11 +1625,13 @@ func (h *Rol_pagesHandler) getAggregateData(ctx context.Context, dbConn *sqlx.DB return aggregate, nil } +// logAndRespondError logs an error and sends an error response func (h *Rol_pagesHandler) 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) } +// respondError sends an error response func (h *Rol_pagesHandler) respondError(c *gin.Context, message string, err error, statusCode int) { errorMessage := message if gin.Mode() == gin.ReleaseMode { @@ -1567,6 +1640,7 @@ func (h *Rol_pagesHandler) respondError(c *gin.Context, message string, err erro c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) } +// calculateMeta calculates pagination metadata func (h *Rol_pagesHandler) calculateMeta(limit, offset, total int) models.MetaResponse { totalPages, currentPage := 0, 1 if limit > 0 { @@ -1578,3 +1652,171 @@ func (h *Rol_pagesHandler) calculateMeta(limit, offset, total int) models.MetaRe CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, } } + +// ============================================================================= +// UTILITY FUNCTIONS +// ============================================================================= + +// getInt64 safely extracts an int64 from a map[string]interface{} +func getInt64(m map[string]interface{}, key string) int64 { + if val, ok := m[key]; ok { + switch v := val.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + } + } + return 0 +} + +// getString safely extracts a string from a map[string]interface{} +func getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if v, ok := val.(string); ok { + return v + } + } + return "" +} + +// getNullString safely extracts a sql.NullString from a map[string]interface{} +func getNullString(m map[string]interface{}, key string) sql.NullString { + if val, ok := m[key]; ok { + if ns, ok := val.(sql.NullString); ok { + return ns + } + if s, ok := val.(string); ok { + return sql.NullString{String: s, Valid: true} + } + } + return sql.NullString{Valid: false} +} + +// getStringArray safely extracts a pq.StringArray from a map[string]interface{} +func getStringArray(m map[string]interface{}, key string) pq.StringArray { + if val, ok := m[key]; ok { + if arr, ok := val.(pq.StringArray); ok { + // Clean the array elements + cleanedArray := make([]string, 0, len(arr)) + for _, element := range arr { + cleaned := strings.TrimSpace(element) + cleaned = strings.TrimPrefix(cleaned, "{") + cleaned = strings.TrimSuffix(cleaned, "}") + cleaned = strings.Trim(cleaned, `"`) + cleanedArray = append(cleanedArray, cleaned) + } + return pq.StringArray(cleanedArray) + } + + if str, ok := val.(string); ok { + parts := strings.Split(str, ",") + var stringSlice []string + for _, part := range parts { + if t := strings.TrimSpace(part); t != "" { + cleaned := strings.TrimPrefix(t, "{") + cleaned = strings.TrimSuffix(cleaned, "}") + cleaned = strings.Trim(cleaned, `"`) + stringSlice = append(stringSlice, cleaned) + } + } + return pq.StringArray(stringSlice) + } + } + return pq.StringArray{} +} + +// getNullableInt32 safely extracts a models.NullableInt32 from a map[string]interface{} +func getNullableInt32(m map[string]interface{}, key string) models.NullableInt32 { + if val, ok := m[key]; ok { + if v, ok := val.(models.NullableInt32); ok { + return v + } + } + return models.NullableInt32{} +} + +// getBool safely extracts a bool from a map[string]interface{} +func getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if v, ok := val.(bool); ok { + return v + } + } + return false +} + +// getNullBool safely extracts a sql.NullBool from a map[string]interface{} +func getNullBool(m map[string]interface{}, key string) sql.NullBool { + if val, ok := m[key]; ok { + if nb, ok := val.(sql.NullBool); ok { + return nb + } + if b, ok := val.(bool); ok { + return sql.NullBool{Bool: b, Valid: true} + } + } + return sql.NullBool{Valid: false} +} + +// parseCSVParam splits comma-separated query params and trims spaces +func parseCSVParam(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + if t := strings.TrimSpace(p); t != "" { + out = append(out, t) + } + } + return out +} + +// toNullBool converts a *bool to sql.NullBool +func toNullBool(p *bool) sql.NullBool { + if p == nil { + return sql.NullBool{Valid: false} + } + return sql.NullBool{Bool: *p, Valid: true} +} + +// toPQStringArray converts a *[]string to pq.StringArray +func toPQStringArray(p *[]string) pq.StringArray { + if p == nil || len(*p) == 0 { + return pq.StringArray{} + } + return pq.StringArray(*p) +} + +// buildUpdateColumnsFromStruct uses reflection to build update columns for structs with simple pointer fields. +// It requires a map to link struct field names to their corresponding database column names. +func buildUpdateColumnsFromStruct(v interface{}, fieldToColumn map[string]string) ([]string, []interface{}) { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr || val.IsNil() { + return nil, nil + } + val = val.Elem() + typ := val.Type() + + columns := make([]string, 0, len(fieldToColumn)) + values := make([]interface{}, 0, len(fieldToColumn)) + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // We only care about fields defined in our map + fieldName := fieldType.Name + if _, ok := fieldToColumn[fieldName]; !ok { + continue + } + + // Check if the field is a non-nil pointer + if field.Kind() == reflect.Ptr && !field.IsNil() { + columns = append(columns, fieldToColumn[fieldName]) + values = append(values, field.Elem().Interface()) // Get the value the pointer points to + } + } + return columns, values +} diff --git a/internal/models/pages/rol_pages.go b/internal/models/pages/rol_pages.go index 6924301..49b950a 100644 --- a/internal/models/pages/rol_pages.go +++ b/internal/models/pages/rol_pages.go @@ -184,7 +184,7 @@ type PagesCreateRequest struct { //Status string `json:"status" validate:"required,oneof=draft active inactive"` ID *int `json:"id"` Name string `json:"name" validate:"required,min=1,max=20"` - Icon *string `json:"icon" validate:"omitempty,min=1,max=20"` + Icon *string `json:"icon" validate:"omitempty,min=1,max=100"` Url *string `json:"url" validate:"omitempty,min=1,max=100"` Level int `json:"level"` Sort int `json:"sort"` @@ -246,10 +246,10 @@ type PagesUpdateResponse struct { // Update request type PagesUpdateRequest struct { - ID *int `json:"-" validate:"required"` + ID *int `json:"id" validate:"required"` // Status string `json:"status" validate:"required,oneof=draft active inactive"` Name *string `json:"name" validate:"omitempty,min=1,max=20"` - Icon *string `json:"icon" validate:"omitempty,min=1,max=20"` + Icon *string `json:"icon" validate:"omitempty,min=1,max=100"` Url *string `json:"url" validate:"omitempty,min=1,max=100"` Level *int `json:"level" validate:"omitempty,min=1"` Sort *int `json:"sort" validate:"omitempty,min=1"` diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 1ce9c41..e3957f1 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -147,6 +147,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { pagesRolpagesGroup.GET("/", pagesRolpagesHandler.GetRol_pages) pagesRolpagesGroup.POST("/", pagesRolpagesHandler.CreateRol_pages) pagesRolpagesGroup.PUT("/:id", pagesRolpagesHandler.UpdateRol_pages) + pagesRolpagesGroup.PUT("/bulk", pagesRolpagesHandler.UpdateRol_pagesBulk) pagesRolpagesGroup.DELETE("/:id", pagesRolpagesHandler.DeleteRol_pages) }