first commit

This commit is contained in:
meninjar
2026-04-14 01:23:34 +00:00
commit edfaa886ff
443 changed files with 1245931 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
package role
// RolPagesRequest represents the request payload for RolPages
type RolPagesRequest struct {
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level int16 `json:"level" validate:"required"`
Sort int16 `json:"sort"`
Parent *int64 `json:"parent"`
Active bool `json:"active"`
}
type RolPagesResponse struct {
Id int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level int16 `json:"level"`
Sort int16 `json:"sort"`
Parent *int64 `json:"parent"`
Active bool `json:"active"`
}
// RoleAccessResponse represents the response structure for role access with hierarchical menu
type RoleAccessResponse struct {
Roles []string `json:"roles"`
Access []MenuItem `json:"access"`
}
// MenuItem represents a menu item in the access hierarchy
type MenuItem struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level string `json:"level"`
Children []MenuChild `json:"children"`
}
// MenuChild represents a child menu item
type MenuChild struct {
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level string `json:"level"`
}
+18
View File
@@ -0,0 +1,18 @@
package role
// RolPages entity represents the rol_pages table in the database
type RolPages struct {
Id int64 `json:"id" db:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" db:"name"`
Icon string `json:"icon" db:"icon"`
URL string `json:"url" db:"url"`
Level int16 `json:"level" db:"level"`
Sort int16 `json:"sort" db:"sort"`
Parent *int64 `json:"parent" db:"parent"`
Active bool `json:"active" db:"active"`
}
// TableName specifies the table name for RolPages
func (RolPages) TableName() string {
return "role_access.rol_pages"
}
+95
View File
@@ -0,0 +1,95 @@
package role
import (
"fmt"
"time"
rolepPermission "service/internal/master/role/permission"
)
func mapRequestToEntity(req RolPagesRequest) *RolPages {
return &RolPages{
Name: req.Name,
Icon: req.Icon,
URL: req.URL,
Level: req.Level,
Sort: req.Sort,
Parent: req.Parent,
Active: req.Active,
}
}
func mapEntityToResponse(e *RolPages) *RolPagesResponse {
return &RolPagesResponse{
Id: e.Id,
Name: e.Name,
Icon: e.Icon,
URL: e.URL,
Level: e.Level,
Sort: e.Sort,
Parent: e.Parent,
Active: e.Active,
}
}
// MapPermissionToRoleAccessResponse converts RolPermission to RoleAccessResponse structure
func MapPermissionToRoleAccessResponse(roles []string, permissions []*rolepPermission.RolPermission, pages []*RolPages) *RoleAccessResponse {
response := &RoleAccessResponse{
Roles: roles,
Access: []MenuItem{},
}
// Map permissions to create menu items based on pages
// Filter pages yang memiliki permission read = true
accessiblePages := []*RolPages{}
for i, permission := range permissions {
if permission.Read && i < len(pages) {
accessiblePages = append(accessiblePages, pages[i])
}
}
// Group pages by parent ID
parentMap := make(map[int64][]*RolPages)
var rootPages []*RolPages
for _, page := range accessiblePages {
if page.Parent == nil {
rootPages = append(rootPages, page)
} else {
parentMap[*page.Parent] = append(parentMap[*page.Parent], page)
}
}
// Build menu hierarchy
for _, rootPage := range rootPages {
menuItem := MenuItem{
ID: generateUUID(),
Name: rootPage.Name,
Icon: rootPage.Icon,
URL: rootPage.URL,
Level: fmt.Sprintf("%d", rootPage.Level),
}
// Add children if any
if children, exists := parentMap[rootPage.Id]; exists {
for _, child := range children {
menuChild := MenuChild{
Name: child.Name,
Icon: child.Icon,
URL: child.URL,
Level: fmt.Sprintf("%d", child.Level),
}
menuItem.Children = append(menuItem.Children, menuChild)
}
}
response.Access = append(response.Access, menuItem)
}
return response
}
// Helper function to generate UUID (you may want to use a proper UUID library)
func generateUUID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
+172
View File
@@ -0,0 +1,172 @@
package role
import (
"context"
"fmt"
"service/pkg/logger"
"service/pkg/utils/query"
rolepPermission "service/internal/master/role/permission"
"github.com/jmoiron/sqlx"
"gorm.io/gorm"
)
// RoleAccessRepository defines the contract for role access data operations
type RoleAccessRepository interface {
// GetRoleAccess retrieves role access information for a specific user
GetRoleAccess(ctx context.Context, userID string) (*RoleAccessData, error)
// GetRolePermissions retrieves permissions for a specific role
GetRolePermissions(ctx context.Context, roleID int64) ([]*rolepPermission.RolPermission, error)
// GetRolePages retrieves pages accessible by role
GetRolePages(ctx context.Context, roleIDs []int64) ([]*RolPages, error)
// GetUserRoles retrieves roles for a specific user
GetUserRoles(ctx context.Context, userID string) ([]string, error)
}
// RoleAccessData represents the complete role access information
type RoleAccessData struct {
Roles []string
Permissions []*rolepPermission.RolPermission
Pages []*RolPages
}
type roleAccessRepository struct {
db *gorm.DB
qb query.QueryBuilder
dbType query.DBType
}
// NewRoleAccessRepository creates a new instance of RoleAccessRepository
func NewRoleAccessRepository(db *gorm.DB) RoleAccessRepository {
dbType := query.DBTypePostgreSQL
qb := query.NewSQLQueryBuilder(dbType).
SetSecurityOptions(true, 1000).
SetQueryLogging(true).
SetQueryTimeout(30)
return &roleAccessRepository{
db: db,
qb: qb,
dbType: dbType,
}
}
// getSQLXDB extracts *sqlx.DB from *gorm.DB
func (r *roleAccessRepository) getSQLXDB() (*sqlx.DB, error) {
sqlDB, err := r.db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying *sql.DB: %w", err)
}
return sqlx.NewDb(sqlDB, "pgx"), nil
}
// GetRoleAccess retrieves complete role access information for a user
func (r *roleAccessRepository) GetRoleAccess(ctx context.Context, userID string) (*RoleAccessData, error) {
// Get user roles
roles, err := r.GetUserRoles(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user roles: %w", err)
}
if len(roles) == 0 {
return &RoleAccessData{
Roles: []string{},
Permissions: []*rolepPermission.RolPermission{},
Pages: []*RolPages{},
}, nil
}
// For now, we'll use mock role IDs - in real implementation, this should come from database
// Mock role IDs based on role names
var roleIDs []int64
for range roles {
roleIDs = append(roleIDs, 1) // Mock role ID
}
// Get permissions and pages for the roles
permissions, err := r.GetRolePermissions(ctx, 1) // Mock role ID
if err != nil {
return nil, fmt.Errorf("failed to get role permissions: %w", err)
}
pages, err := r.GetRolePages(ctx, roleIDs)
if err != nil {
return nil, fmt.Errorf("failed to get role pages: %w", err)
}
return &RoleAccessData{
Roles: roles,
Permissions: permissions,
Pages: pages,
}, nil
}
// GetRolePermissions retrieves permissions for a specific role
func (r *roleAccessRepository) GetRolePermissions(ctx context.Context, roleID int64) ([]*rolepPermission.RolPermission, error) {
sqlxDB, err := r.getSQLXDB()
if err != nil {
return nil, err
}
// For now, return all permissions - in real implementation, this should filter by role
q := query.DynamicQuery{
From: "rol_permission",
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{
query.CreateFilter("DeletedAt", query.OpNull, nil),
},
}},
Limit: 100,
}
var results []*rolepPermission.RolPermission
if err := r.qb.ExecuteQuery(ctx, sqlxDB, q, &results); err != nil {
return nil, fmt.Errorf("failed to execute role permissions query: %w", err)
}
return results, nil
}
// GetRolePages retrieves pages accessible by role
func (r *roleAccessRepository) GetRolePages(ctx context.Context, roleIDs []int64) ([]*RolPages, error) {
sqlxDB, err := r.getSQLXDB()
if err != nil {
return nil, err
}
// Get all active pages - in real implementation, this should join with role-page permissions
q := query.DynamicQuery{
From: "role_access.rol_pages",
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("Active", true),
},
}},
Sort: []query.SortField{
query.CreateAscSort("Level"),
query.CreateAscSort("Sort"),
},
Limit: 100,
}
var results []*RolPages
if err := r.qb.ExecuteQuery(ctx, sqlxDB, q, &results); err != nil {
return nil, fmt.Errorf("failed to execute role pages query: %w", err)
}
return results, nil
}
// GetUserRoles retrieves roles for a specific user
func (r *roleAccessRepository) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
// Mock implementation - in real implementation, this should query user-role mapping table
// For now, return mock roles based on user ID
logger.Default().Info("Getting user roles", logger.String("userID", userID))
// Mock roles - in real implementation, get from database
mockRoles := []string{"admin1", "admin2"}
return mockRoles, nil
}
+54
View File
@@ -0,0 +1,54 @@
package role
import (
"context"
"service/pkg/errors"
)
// RoleAccessService defines the contract for role access operations
type RoleAccessService interface {
GetRoleAccess(ctx context.Context, userID string) (*RoleAccessResponse, error)
}
// roleAccessService implements RoleAccessService
type roleAccessService struct {
repo RoleAccessRepository
}
// NewRoleAccessService creates a new instance of RoleAccessService
func NewRoleAccessService(repo RoleAccessRepository) RoleAccessService {
return &roleAccessService{
repo: repo,
}
}
// GetRoleAccess retrieves role access information for a user
func (s *roleAccessService) GetRoleAccess(ctx context.Context, userID string) (*RoleAccessResponse, error) {
if userID == "" {
return nil, errors.NewValidationError().Message("User ID is required").Metadata("userID", userID).Build()
}
// Get role access data from repository
roleAccessData, err := s.repo.GetRoleAccess(ctx, userID)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve role access data").Cause(err).Build()
}
// Map the data to response format
response := MapPermissionToRoleAccessResponse(
roleAccessData.Roles,
roleAccessData.Permissions,
roleAccessData.Pages,
)
return response, nil
}
// GetUserRoles retrieves roles for a specific user
func (s *roleAccessService) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
if userID == "" {
return nil, errors.NewValidationError().Message("User ID is required").Metadata("userID", userID).Build()
}
return s.repo.GetUserRoles(ctx, userID)
}
+23
View File
@@ -0,0 +1,23 @@
package master
import "time"
// RoleAccessRolMasterRequest represents the request payload for RoleAccessRolMaster
type RoleMasterRequest struct {
Id *int64 `json:"id"`
Name *string `json:"name"`
Active *bool `json:"active"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Slug *string `json:"slug"`
}
// RoleMasterResponse represents the response payload for RoleMaster
type RoleMasterResponse struct {
Id int64 `json:"id"`
Name *string `json:"name"`
Active *bool `json:"active"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Slug *string `json:"slug"`
}
+20
View File
@@ -0,0 +1,20 @@
package master
import (
"time"
)
// RoleMaster entity represents the role_access.rol_master table in the database
type RoleMaster struct {
Id int64 `json:"id" db:"id" gorm:"primaryKey;autoIncrement"`
Name *string `json:"name" db:"name"`
Active *bool `json:"active" db:"active"`
CreatedAt *time.Time `json:"created_at" db:"created_at"`
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
Slug *string `json:"slug" db:"slug"`
}
// TableName specifies the table name for RoleMaster
func (RoleMaster) TableName() string {
return "role_access.rol_master"
}
+29
View File
@@ -0,0 +1,29 @@
package master
// mapRequestToEntity converts RoleMasterRequest to *RoleMaster entity
func mapRequestToEntity(req RoleMasterRequest) *RoleMaster {
entity := &RoleMaster{
Name: req.Name,
Active: req.Active,
Slug: req.Slug,
CreatedAt: req.CreatedAt,
UpdatedAt: req.UpdatedAt,
}
if req.Id != nil {
entity.Id = *req.Id
}
return entity
}
// mapEntityToResponse converts *RoleMaster entity to *RoleMasterResponse DTO
func mapEntityToResponse(e *RoleMaster) *RoleMasterResponse {
return &RoleMasterResponse{
Id: e.Id,
Name: e.Name,
Active: e.Active,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Slug: e.Slug,
}
}
+258
View File
@@ -0,0 +1,258 @@
package master
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"service/internal/infrastructure/database"
"service/pkg/logger"
"service/pkg/utils/query"
"github.com/jmoiron/sqlx"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type CommandRepository interface {
Create(ctx context.Context, entity *RoleMaster) error
Update(ctx context.Context, entity *RoleMaster) error
Upsert(ctx context.Context, entity *RoleMaster) error
Delete(ctx context.Context, id int64) error
}
type QueryRepository interface {
FindAll(ctx context.Context, limit, offset int) ([]RoleMaster, int64, error)
FindByID(ctx context.Context, id int64) (*RoleMaster, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RoleMaster, int64, error)
}
type repository struct {
dbManager database.Service
dbName string
qb query.QueryBuilder
dbType query.DBType
allowedColumnsMap map[string]bool
}
func NewCommandRepository(dbManager database.Service, dbName string) CommandRepository {
return NewRepository(dbManager, dbName)
}
func NewQueryRepository(dbManager database.Service, dbName string) QueryRepository {
return NewRepository(dbManager, dbName)
}
func NewRepository(dbManager database.Service, dbName string) *repository {
dbType := query.DBTypePostgreSQL
allowedColumns := []string{
"id",
"name",
"active",
"created_at",
"updated_at",
}
qb := query.NewSQLQueryBuilder(dbType).
SetSecurityOptions(true, 1000).
SetQueryLogging(true).
SetQueryTimeout(30).
SetAllowedColumns(allowedColumns)
allowedMap := make(map[string]bool)
for _, col := range allowedColumns {
allowedMap[col] = true
}
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType, allowedColumnsMap: allowedMap}
}
// isColumnAllowed memvalidasi apakah kolom diizinkan untuk digunakan
func (r *repository) isColumnAllowed(column string) bool {
return r.allowedColumnsMap[column]
}
// getWriteGormDB extracts *gorm.DB for Write/Command operations
func (r *repository) getWriteGormDB() (*gorm.DB, error) {
return r.dbManager.GetGormDB(r.dbName)
}
// getReadSQLXDB extracts *sqlx.DB from read replicas for Read/Query operations
func (r *repository) getReadSQLXDB() (*sqlx.DB, error) {
db, err := r.dbManager.GetReadDB(r.dbName)
if err != nil {
return nil, fmt.Errorf("failed to get read db: %w", err)
}
// Gunakan Unsafe() agar sqlx mengabaikan kolom hasil query yang tidak terdapat di dalam struct
return sqlx.NewDb(db, "pgx").Unsafe(), nil
}
// FindAll fetches all RoleMaster with pagination
func (r *repository) FindAll(ctx context.Context, limit, offset int) ([]RoleMaster, int64, error) {
db, err := r.getReadSQLXDB()
if err != nil {
return nil, 0, err
}
dq := query.DynamicQuery{
From: "role_access.rol_master",
Limit: limit, Offset: offset,
Sort: []query.SortField{query.CreateAscSort("id")},
}
var results []RoleMaster
if err := r.qb.ExecuteQuery(ctx, db, dq, &results); err != nil {
return nil, 0, fmt.Errorf("failed to execute find all query: %w", err)
}
count, err := r.qb.ExecuteCount(ctx, db, dq)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute count query: %w", err)
}
return results, count, nil
}
// FindByID fetches a single RoleMaster by ID
func (r *repository) FindByID(ctx context.Context, id int64) (*RoleMaster, error) {
db, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
var result RoleMaster
q := query.DynamicQuery{
From: "role_access.rol_master",
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("id", id),
},
}},
Limit: 1,
}
if err := r.qb.ExecuteQueryRow(ctx, db, q, &result); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // Return nil, nil jika data tidak ada, jangan lempar error query
}
// Wrapping error origin
return nil, fmt.Errorf("failed to fetch RoleAccessRolMaster: %w", err)
}
return &result, nil
}
// Search fetches RoleMaster based on dynamic filters and sorting
func (r *repository) Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RoleMaster, int64, error) {
db, err := r.getReadSQLXDB()
if err != nil {
return nil, 0, err
}
var dynamicFilters []query.DynamicFilter
for k, v := range filters {
colName := strings.ToLower(k)
if !r.isColumnAllowed(colName) {
continue
}
switch val := v.(type) {
case string:
if val != "" {
if colName == "name" {
dynamicFilters = append(dynamicFilters, query.CreateFilter(colName, query.OpILike, "%"+val+"%"))
} else if colName == "active" {
if boolVal, err := strconv.ParseBool(val); err == nil && (val == "true" || val == "false" || val == "1" || val == "0") {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, boolVal))
} else {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, val))
}
} else {
if intVal, err := strconv.Atoi(val); err == nil {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, intVal))
} else {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, val))
}
}
}
default:
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, val))
}
}
var sortFields []query.SortField
for _, sort := range sorts {
colName := strings.ToLower(sort.Column)
if r.isColumnAllowed(colName) {
sortFields = append(sortFields, query.SortField{
Column: colName,
Order: sort.Order,
})
}
}
// Jika tidak ada sort yang valid, gunakan default
if len(sortFields) == 0 {
sortFields = []query.SortField{query.CreateAscSort("id")}
}
q := query.DynamicQuery{
From: "role_access.rol_master",
Fields: []query.SelectField{
{Expression: "*"},
},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
Limit: limit, Offset: offset,
Sort: sortFields,
}
logger.Default().Info("Built search query", logger.String("request", fmt.Sprintf("%+v", q)))
var results []RoleMaster
if err := r.qb.ExecuteQuery(ctx, db, q, &results); err != nil {
return nil, 0, fmt.Errorf("failed to execute search query: %w", err)
}
count, err := r.qb.ExecuteCount(ctx, db, q)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute search count query: %w", err)
}
return results, count, nil
}
// Create inserts a new RoleMaster record
func (r *repository) Create(ctx context.Context, entity *RoleMaster) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Create(entity).Error
}
func (r *repository) Update(ctx context.Context, entity *RoleMaster) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Save(entity).Error
}
func (r *repository) Upsert(ctx context.Context, entity *RoleMaster) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}}, // Kolom acuan untuk konflik
UpdateAll: true, // Update seluruh kolom jika ada konflik
}).Create(entity).Error
}
func (r *repository) Delete(ctx context.Context, id int64) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Delete(&RoleMaster{}, id).Error
}
+212
View File
@@ -0,0 +1,212 @@
package master
import (
"context"
"service/internal/infrastructure/cache"
"service/pkg/errors"
"service/pkg/utils/query"
"strings"
"gorm.io/gorm"
)
type Service interface {
GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error)
GetDetail(ctx context.Context, id int64) (*RoleMasterResponse, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error)
Create(ctx context.Context, req RoleMasterRequest) (*RoleMasterResponse, error)
Update(ctx context.Context, id int64, req RoleMasterRequest) (*RoleMasterResponse, error)
Upsert(ctx context.Context, req RoleMasterRequest) (*RoleMasterResponse, error)
Delete(ctx context.Context, id int64) error
}
type service struct {
cmdRepo CommandRepository
queryRepo QueryRepository
cache *cache.Manager
}
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, cacheManager *cache.Manager) Service {
return &service{cmdRepo: cmdRepo, queryRepo: queryRepo, cache: cacheManager}
}
func (s *service) GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error) {
filters := map[string]interface{}{}
if activeFilter != nil {
filters["active"] = *activeFilter
}
return s.Search(ctx, filters, sorts, limit, offset)
}
func (s *service) GetDetail(ctx context.Context, id int64) (*RoleMasterResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
entity, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RoleMaster detail").Cause(err).Build()
}
if entity == nil {
return nil, errors.NotFoundError().Message("RoleMaster not found").Metadata("id", id).Build()
}
return mapEntityToResponse(entity), nil
}
func (s *service) Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error) {
if limit == 0 {
limit = 10
} else if limit < -1 || limit > 100 {
limit = 10
}
if offset < 0 {
offset = 0
}
// Konversi sorts string ke SortField
var sortFields []query.SortField
for _, sort := range sorts {
if sort == "" {
continue
}
// Default ASC, tandai dengan - untuk DESC
order := "ASC"
column := sort
if strings.HasPrefix(sort, "-") {
order = "DESC"
column = strings.TrimPrefix(sort, "-")
} else if strings.HasPrefix(sort, "+") {
column = strings.TrimPrefix(sort, "+")
}
// Validasi kolom yang diizinkan (lowercase untuk mapping)
allowedColumns := map[string]bool{
"id": true,
"name": true,
"active": true,
"created_at": true,
"updated_at": true,
"select": true,
}
if allowedColumns[column] {
sortFields = append(sortFields, query.SortField{
Column: column,
Order: order,
})
}
}
// Jika tidak ada sort yang valid, gunakan default
if len(sortFields) == 0 {
sortFields = []query.SortField{query.CreateAscSort("id")}
}
entities, total, err := s.queryRepo.Search(ctx, filters, sortFields, limit, offset)
if err != nil {
return nil, errors.InternalError().Message("Failed to search RoleMasters").Cause(err).Build()
}
responses := make([]*RoleMasterResponse, len(entities))
for i, entity := range entities {
responses[i] = mapEntityToResponse(&entity)
}
return map[string]interface{}{
"data": responses, "total": total, "limit": limit, "offset": offset,
}, nil
}
func (s *service) Create(ctx context.Context, req RoleMasterRequest) (*RoleMasterResponse, error) {
// TODO: Add validation here if needed
entity := mapRequestToEntity(req)
if err := s.cmdRepo.Create(ctx, entity); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RoleMaster with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to create RoleMaster").Cause(err).Build()
}
// Re-fetch to get the complete created entity
createdEntity, err := s.queryRepo.FindByID(ctx, entity.Id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve newly created RoleMaster").Cause(err).Build()
}
return mapEntityToResponse(createdEntity), nil
}
func (s *service) Update(ctx context.Context, id int64, req RoleMasterRequest) (*RoleMasterResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah record ada
existing, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RoleMaster").Cause(err).Build()
}
if existing == nil {
return nil, errors.NotFoundError().Message("RoleMaster not found").Metadata("id", id).Build()
}
// Update fields from request on the existing entity
existing.Name = req.Name
existing.Active = req.Active
existing.Slug = req.Slug
existing.CreatedAt = req.CreatedAt
existing.UpdatedAt = req.UpdatedAt
if err := s.cmdRepo.Update(ctx, existing); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RoleMaster with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to update RoleMaster").Cause(err).Build()
}
// TODO: Add cache invalidation logic here
return mapEntityToResponse(existing), nil
}
func (s *service) Upsert(ctx context.Context, req RoleMasterRequest) (*RoleMasterResponse, error) {
entity := mapRequestToEntity(req)
// Lakukan proses Create dengan skenario OnConflict Do Update
if err := s.cmdRepo.Upsert(ctx, entity); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RoleMaster with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to upsert RoleMaster").Cause(err).Build()
}
// Ambil kembali data hasil Upsert secara penuh dari database
upsertedEntity, err := s.queryRepo.FindByID(ctx, entity.Id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve upserted RoleMaster").Cause(err).Build()
}
return mapEntityToResponse(upsertedEntity), nil
}
func (s *service) Delete(ctx context.Context, id int64) error {
if id <= 0 {
return errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah record ada sebelum dihapus untuk idempotency dan pre-delete logic
existing, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return errors.InternalError().Message("Failed to retrieve RoleMaster before deletion").Cause(err).Build()
}
if existing == nil {
return nil
} // Idempotent: jika tidak ada, anggap berhasil
if err := s.cmdRepo.Delete(ctx, id); err != nil {
return errors.InternalError().Message("Failed to delete RoleMaster").Cause(err).Build()
}
// TODO: Tambahkan invalidasi cache di sini jika ada
return nil
}
+37
View File
@@ -0,0 +1,37 @@
package pages
// RolPagesRequest represents the request payload for RolPages
type RolPagesRequest struct {
Name string `json:"name" validate:"required,max=20"`
Icon string `json:"icon" validate:"max=100"`
URL string `json:"url" validate:"required"`
Level int16 `json:"level" validate:"required,min=0"`
Sort int16 `json:"sort" validate:"required,min=0"`
Parent *int64 `json:"parent"`
Active bool `json:"active"`
}
// RolPagesResponse represents the response payload for RolPages
type RolPagesResponse struct {
Id int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level int16 `json:"level"`
Sort int16 `json:"sort"`
Parent *int64 `json:"parent,omitempty"`
Active bool `json:"active"`
Children []*RolPagesResponse `json:"children,omitempty"`
}
// RolPagesTreeResponse represents hierarchical menu structure
type RolPagesTreeResponse struct {
Id int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level int16 `json:"level"`
Sort int16 `json:"sort"`
Active bool `json:"active"`
Children []*RolPagesTreeResponse `json:"children,omitempty"`
}
+28
View File
@@ -0,0 +1,28 @@
package pages
import (
"time"
)
// RolPages entity represents the rol_pages table in the database
type RolPages struct {
Id int64 `json:"id" db:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" db:"name"`
Icon string `json:"icon" db:"icon"`
URL string `json:"url" db:"url"`
Level int16 `json:"level" db:"level"`
Sort int16 `json:"sort" db:"sort"`
Parent *int64 `json:"parent" db:"parent"`
Active bool `json:"active" db:"active"`
CreatedAt *time.Time `json:"created_at" db:"created_at"`
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
// Relasi untuk hirarki menu
Children []RolPages `json:"children" gorm:"-"`
ParentRef *RolPages `json:"parent_ref,omitempty" gorm:"-" db:"-"`
}
// TableName specifies the table name for RolPages
func (RolPages) TableName() string {
return "role_access.rol_pages"
}
+89
View File
@@ -0,0 +1,89 @@
package pages
// mapRequestToEntity converts RolPagesRequest to *RolPages entity
func mapRequestToEntity(req RolPagesRequest) *RolPages {
return &RolPages{
Name: req.Name,
Icon: req.Icon,
URL: req.URL,
Level: req.Level,
Sort: req.Sort,
Parent: req.Parent,
Active: req.Active,
}
}
// mapEntityToResponse converts *RolPages entity to *RolPagesResponse DTO
func mapEntityToResponse(e *RolPages) *RolPagesResponse {
return &RolPagesResponse{
Id: e.Id,
Name: e.Name,
Icon: e.Icon,
URL: e.URL,
Level: e.Level,
Sort: e.Sort,
Parent: e.Parent,
Active: e.Active,
}
}
// mapEntityToTreeResponse converts *RolPages entity to *RolPagesTreeResponse DTO
func mapEntityToTreeResponse(e *RolPages) *RolPagesTreeResponse {
return &RolPagesTreeResponse{
Id: e.Id,
Name: e.Name,
Icon: e.Icon,
URL: e.URL,
Level: e.Level,
Sort: e.Sort,
Active: e.Active,
}
}
// buildTreeHierarchy membangun struktur hirarki dari flat list
func buildTreeHierarchy(pages []*RolPages) []*RolPagesTreeResponse {
pageMap := make(map[int64]*RolPagesTreeResponse)
var roots []*RolPagesTreeResponse
// Convert semua pages ke TreeResponse dan buat map
for _, page := range pages {
treeResponse := mapEntityToTreeResponse(page)
pageMap[page.Id] = treeResponse
}
// Bangun relasi parent-child
for _, page := range pages {
if page.Parent == nil {
// Ini adalah root node
roots = append(roots, pageMap[page.Id])
} else {
// Ini adalah child node
if parent, exists := pageMap[*page.Parent]; exists {
parent.Children = append(parent.Children, pageMap[page.Id])
}
}
}
// Sort children berdasarkan sort order
for _, page := range pageMap {
if len(page.Children) > 0 {
sortChildren(page.Children)
}
}
// Sort root nodes berdasarkan sort order
sortChildren(roots)
return roots
}
// sortChildren mengurutkan children berdasarkan field Sort
func sortChildren(children []*RolPagesTreeResponse) {
for i := 0; i < len(children); i++ {
for j := i + 1; j < len(children); j++ {
if children[i].Sort > children[j].Sort {
children[i], children[j] = children[j], children[i]
}
}
}
}
+227
View File
@@ -0,0 +1,227 @@
package pages
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"service/internal/infrastructure/database"
"service/pkg/utils/query"
"github.com/jmoiron/sqlx"
"gorm.io/gorm"
)
// CommandRepository menangani operasi Write (Create, Update, Delete) ke Master DB
type CommandRepository interface {
Create(ctx context.Context, entity *RolPages) error
Update(ctx context.Context, entity *RolPages) error
Delete(ctx context.Context, id int64) error
}
// QueryRepository menangani operasi Read (Find, Search) ke Read-Replica DB
type QueryRepository interface {
FindByID(ctx context.Context, id int64) (*RolPages, error)
GetChildren(ctx context.Context, parentID int64) ([]RolPages, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RolPages, int64, error)
}
type repository struct {
dbManager database.Service
dbName string
qb query.QueryBuilder
dbType query.DBType
allowedColumnsMap map[string]bool
}
// Konstruktor Baru CQRS
func NewCommandRepository(dbManager database.Service, dbName string) CommandRepository {
return NewRepository(dbManager, dbName)
}
func NewQueryRepository(dbManager database.Service, dbName string) QueryRepository {
return NewRepository(dbManager, dbName)
}
func NewRepository(dbManager database.Service, dbName string) *repository {
dbType := query.DBTypePostgreSQL
allowedColumns := []string{
"id",
"name",
"icon",
"url",
"level",
"sort",
"parent",
"active",
"created_at",
"updated_at",
}
qb := query.NewSQLQueryBuilder(dbType).
SetSecurityOptions(true, 1000).
SetQueryLogging(true).
SetQueryTimeout(30).
SetAllowedColumns(allowedColumns)
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType, allowedColumnsMap: createAllowedColumnsMap(allowedColumns)}
}
// Helper function untuk membuat map dari allowed columns
func createAllowedColumnsMap(columns []string) map[string]bool {
result := make(map[string]bool)
for _, col := range columns {
result[col] = true
}
return result
}
// isColumnAllowed memvalidasi apakah kolom diizinkan untuk digunakan
func (r *repository) isColumnAllowed(column string) bool {
return r.allowedColumnsMap[column]
}
// Helper mengambil koneksi Master dari DB Manager
func (r *repository) getWriteGormDB() (*gorm.DB, error) {
return r.dbManager.GetGormDB(r.dbName)
}
// Helper mengambil koneksi Read-Replica dari DB Manager
func (r *repository) getReadSQLXDB() (*sqlx.DB, error) {
sqlDB, err := r.dbManager.GetReadDB(r.dbName)
if err != nil {
return nil, fmt.Errorf("failed to get read db: %w", err)
}
return sqlx.NewDb(sqlDB, "pgx"), nil
}
// --- Implementasi Write Model ---
func (r *repository) Create(ctx context.Context, entity *RolPages) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Create(entity).Error
}
func (r *repository) Update(ctx context.Context, entity *RolPages) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Save(entity).Error
}
func (r *repository) Delete(ctx context.Context, id int64) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Delete(&RolPages{}, id).Error
}
// --- Implementasi Read Model ---
func (r *repository) FindByID(ctx context.Context, id int64) (*RolPages, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
q := query.DynamicQuery{
From: "role_access.rol_pages",
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{query.CreateEqualFilter("id", id)},
}},
Limit: 1,
}
var entity RolPages
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &entity); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("failed to fetch RolPages by ID: %w", err)
}
return &entity, nil
}
func (r *repository) GetChildren(ctx context.Context, parentID int64) ([]RolPages, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
q := query.DynamicQuery{
From: "role_access.rol_pages",
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{query.CreateEqualFilter("parent", parentID)},
}},
Sort: []query.SortField{query.CreateAscSort("sort")},
}
var entities []RolPages
if err := r.qb.ExecuteQuery(ctx, sqlxDB, q, &entities); err != nil {
return nil, fmt.Errorf("failed to fetch RolPages children: %w", err)
}
return entities, nil
}
func (r *repository) Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RolPages, int64, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, 0, err
}
var dynamicFilters []query.DynamicFilter
for k, v := range filters {
// Normalisasi nama kolom menjadi lowercase agar sesuai dengan allowedColumns
colName := strings.ToLower(k)
if !r.isColumnAllowed(colName) {
continue
}
switch val := v.(type) {
case string:
if val != "" {
// Gunakan OpILike hanya untuk kolom yang bertipe teks (string)
if colName == "name" || colName == "icon" || colName == "url" {
dynamicFilters = append(dynamicFilters, query.CreateFilter(colName, query.OpILike, "%"+val+"%"))
} else {
if boolVal, err := strconv.ParseBool(val); err == nil && (val == "true" || val == "false" || val == "1" || val == "0") {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, boolVal))
} else if intVal, err := strconv.Atoi(val); err == nil {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, intVal))
} else {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, val))
}
}
}
default:
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(colName, val))
}
}
if len(sorts) == 0 {
sorts = []query.SortField{query.CreateAscSort("id")}
}
q := query.DynamicQuery{
From: "role_access.rol_pages",
Fields: []query.SelectField{{Expression: "*"}},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
Limit: limit,
Offset: offset,
Sort: sorts,
}
var entities []RolPages
if err := r.qb.ExecuteQuery(ctx, sqlxDB, q, &entities); err != nil {
return nil, 0, fmt.Errorf("failed to execute search query: %w", err)
}
count, err := r.qb.ExecuteCount(ctx, sqlxDB, q)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute search count query: %w", err)
}
return entities, count, nil
}
+308
View File
@@ -0,0 +1,308 @@
package pages
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"service/internal/infrastructure/cache"
"service/pkg/errors"
"service/pkg/logger"
"service/pkg/utils/query"
"strings"
"time"
"gorm.io/gorm"
)
type Service interface {
GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error)
GetDetail(ctx context.Context, id int64) (*RolPagesResponse, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error)
Create(ctx context.Context, req RolPagesRequest) (*RolPagesResponse, error)
Update(ctx context.Context, id int64, req RolPagesRequest) (*RolPagesResponse, error)
Delete(ctx context.Context, id int64) error
GetTree(ctx context.Context, activeFilter *bool) ([]*RolPagesTreeResponse, error)
GetTreeByLevel(ctx context.Context, level int16, activeFilter *bool) ([]*RolPagesTreeResponse, error)
}
type service struct {
cmdRepo CommandRepository
queryRepo QueryRepository
cache *cache.Manager
}
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, cacheManager *cache.Manager) Service {
return &service{cmdRepo: cmdRepo, queryRepo: queryRepo, cache: cacheManager}
}
func (s *service) GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error) {
filters := map[string]interface{}{}
if activeFilter != nil {
filters["active"] = *activeFilter
}
return s.Search(ctx, filters, sorts, limit, offset)
}
func (s *service) GetDetail(ctx context.Context, id int64) (*RolPagesResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
entity, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RolPages detail").Cause(err).Build()
}
if entity == nil {
return nil, errors.NotFoundError().Message("RolPages not found").Metadata("id", id).Build()
}
response := mapEntityToResponse(entity)
// Ambil children jika ada
children, err := s.queryRepo.GetChildren(ctx, id)
if err == nil && len(children) > 0 {
response.Children = make([]*RolPagesResponse, len(children))
for i := range children {
response.Children[i] = mapEntityToResponse(&children[i])
}
}
return response, nil
}
func (s *service) Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error) {
if limit == 0 {
limit = 10
} else if limit < -1 || limit > 100 {
limit = 10
}
if offset < 0 {
offset = 0
}
// --- 1. Implementasi Caching (Check) ---
filterBytes, _ := json.Marshal(filters)
sortBytes, _ := json.Marshal(sorts)
hashInput := fmt.Sprintf("%s|%s|%d|%d", string(filterBytes), string(sortBytes), limit, offset)
hash := sha256.Sum256([]byte(hashInput))
cacheKey := fmt.Sprintf("role_pages_search_v2:%s", hex.EncodeToString(hash[:]))
if s.cache != nil {
var strData string
if err := s.cache.Get(ctx, cacheKey, &strData); err == nil && strData != "" {
var cachedData struct {
Data []*RolPagesResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
if err := json.Unmarshal([]byte(strData), &cachedData); err == nil {
logger.Default().Debug("Cache hit for Role Pages Search", logger.String("key", cacheKey))
return map[string]interface{}{
"data": cachedData.Data,
"total": cachedData.Total,
"limit": cachedData.Limit,
"offset": cachedData.Offset,
}, nil
}
}
}
// Konversi sorts string ke SortField
var sortFields []query.SortField
for _, sort := range sorts {
if sort == "" {
continue
}
// Default ASC, tandai dengan - untuk DESC
order := "ASC"
column := sort
if strings.HasPrefix(sort, "-") {
order = "DESC"
column = strings.TrimPrefix(sort, "-")
} else if strings.HasPrefix(sort, "+") {
column = strings.TrimPrefix(sort, "+")
}
// Validasi kolom yang diizinkan (lowercase untuk mapping)
allowedColumns := map[string]bool{
"id": true,
"name": true,
"level": true,
"sort": true,
"active": true,
"parent": true,
"icon": true,
"url": true,
"created_at": true,
"updated_at": true,
}
if allowedColumns[column] {
sortFields = append(sortFields, query.SortField{
Column: column,
Order: order,
})
}
}
// Jika tidak ada sort yang valid, gunakan default
if len(sortFields) == 0 {
sortFields = []query.SortField{query.CreateAscSort("id")}
}
entities, total, err := s.queryRepo.Search(ctx, filters, sortFields, limit, offset)
if err != nil {
return nil, errors.InternalError().Message("Failed to search RolPagess").Cause(err).Build()
}
responses := make([]*RolPagesResponse, len(entities))
for i := range entities {
responses[i] = mapEntityToResponse(&entities[i])
}
responseMap := map[string]interface{}{
"data": responses, "total": total, "limit": limit, "offset": offset,
}
// --- 2. Implementasi Caching (Set) ---
if s.cache != nil {
if bytes, err := json.Marshal(responseMap); err == nil {
_ = s.cache.Set(ctx, cacheKey, string(bytes), 5*time.Minute)
}
}
return responseMap, nil
}
func (s *service) Create(ctx context.Context, req RolPagesRequest) (*RolPagesResponse, error) {
// TODO: Add validation here if needed
entity := mapRequestToEntity(req)
if err := s.cmdRepo.Create(ctx, entity); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RolPages with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to create RolPages").Cause(err).Build()
}
if s.cache != nil {
_ = s.cache.Delete(ctx, "role_pages_search_v2:*")
}
return mapEntityToResponse(entity), nil
}
func (s *service) GetTree(ctx context.Context, activeFilter *bool) ([]*RolPagesTreeResponse, error) {
filters := map[string]interface{}{}
if activeFilter != nil {
filters["active"] = *activeFilter
}
pages, _, err := s.queryRepo.Search(ctx, filters, []query.SortField{query.CreateAscSort("sort"), query.CreateAscSort("id")}, -1, 0)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve pages").Cause(err).Build()
}
// Konversi []RolPages ke []*RolPages
pagePointers := make([]*RolPages, len(pages))
for i := range pages {
pagePointers[i] = &pages[i]
}
return buildTreeHierarchy(pagePointers), nil
}
func (s *service) GetTreeByLevel(ctx context.Context, level int16, activeFilter *bool) ([]*RolPagesTreeResponse, error) {
filters := map[string]interface{}{"level": level}
if activeFilter != nil {
filters["active"] = *activeFilter
}
pages, _, err := s.queryRepo.Search(ctx, filters, []query.SortField{query.CreateAscSort("sort"), query.CreateAscSort("id")}, -1, 0)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve pages by level").Cause(err).Build()
}
// Konversi []RolPages ke []*RolPages
pagePointers := make([]*RolPages, len(pages))
for i := range pages {
pagePointers[i] = &pages[i]
}
return buildTreeHierarchy(pagePointers), nil
}
func (s *service) Update(ctx context.Context, id int64, req RolPagesRequest) (*RolPagesResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah record ada
existing, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RolPages").Cause(err).Build()
}
if existing == nil {
return nil, errors.NotFoundError().Message("RolPages not found").Metadata("id", id).Build()
}
// Validasi parent jika berubah dan tidak boleh circular reference
if req.Parent != nil && *req.Parent != id {
if *req.Parent == id {
return nil, errors.NewValidationError().Message("Cannot set self as parent").Build()
}
parent, err := s.queryRepo.FindByID(ctx, *req.Parent)
if err != nil {
return nil, errors.InternalError().Message("Failed to validate parent").Cause(err).Build()
}
if parent == nil {
return nil, errors.NotFoundError().Message("Parent page not found").Metadata("parent_id", *req.Parent).Build()
}
}
// Update entity
existing.Name = req.Name
existing.Icon = req.Icon
existing.URL = req.URL
existing.Level = req.Level
existing.Sort = req.Sort
existing.Parent = req.Parent
existing.Active = req.Active
if err := s.cmdRepo.Update(ctx, existing); err != nil {
return nil, errors.InternalError().Message("Failed to update RolPages").Cause(err).Build()
}
if s.cache != nil {
_ = s.cache.Delete(ctx, "role_pages_search_v2:*")
}
return mapEntityToResponse(existing), nil
}
func (s *service) Delete(ctx context.Context, id int64) error {
if id <= 0 {
return errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah memiliki children
children, err := s.queryRepo.GetChildren(ctx, id)
if err != nil {
return errors.InternalError().Message("Failed to check children").Cause(err).Build()
}
if len(children) > 0 {
return errors.NewValidationError().Message("Cannot delete page with children").Metadata("children_count", len(children)).Build()
}
if err := s.cmdRepo.Delete(ctx, id); err != nil {
return errors.InternalError().Message("Failed to delete RolPages").Cause(err).Build()
}
if s.cache != nil {
_ = s.cache.Delete(ctx, "role_pages_search_v2:*")
}
return nil
}
+116
View File
@@ -0,0 +1,116 @@
package permission
// RolPermissionRequest represents the request payload for RolPermission
type RolPermissionRequest struct {
Create bool `json:"create"`
Read bool `json:"read"`
Update bool `json:"update"`
Disable bool `json:"disable"`
Delete bool `json:"delete"`
Active bool `json:"active"`
FkRolPagesId int64 `json:"fk_rol_pages_id"`
RoleKeycloak string `json:"role_keycloak"`
GroupKeycloak string `json:"group_keycloak"`
}
// PermissionTreeResponse represents the complete permission tree response
type PermissionTreeResponse struct {
Role string `json:"role"`
Group []string `json:"group"`
Access []*PermissionAccessItem `json:"access"`
}
// PermissionAccessItem represents individual page with permission
type PermissionAccessItem struct {
Id int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Level int16 `json:"level"`
Sort int16 `json:"sort"`
Active bool `json:"active"`
Permission *PermissionDetail `json:"permission"`
Children []*PermissionAccessItem `json:"children,omitempty"`
}
// PermissionResponse represents permission details
type PermissionResponse struct {
Id int64 `json:"id"`
Create bool `json:"create"`
Read bool `json:"read"`
Update bool `json:"update"`
Delete bool `json:"delete"`
Disable bool `json:"disable"`
}
// RolPermissionResponse represents the response payload for RolPermission
type RolPermissionResponse struct {
Id int64 `json:"id"`
Create bool `json:"create"`
Read bool `json:"read"`
Update bool `json:"update"`
Disable bool `json:"disable"`
Delete bool `json:"delete"`
Active bool `json:"active"`
FkRolPagesId int64 `json:"fk_rol_pages_id"`
RoleMasterName *string `json:"role_master"`
RoleKeycloak string `json:"-"`
GroupKeycloak string `json:"group_keycloak"`
}
// PermissionDetail represents permission details for a page
type PermissionDetail struct {
Create bool `json:"create"`
Read bool `json:"read"`
Update bool `json:"update"`
Delete bool `json:"delete"`
Disable bool `json:"disable"`
}
// PageAccess represents page access with permission
type PageAccess struct {
Id int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Group string `json:"group_keycloak"`
Level int16 `json:"level"`
Sort int16 `json:"sort"`
Active bool `json:"active"`
Permission *PermissionDetail `json:"permission,omitempty"`
Children []*PageAccess `json:"children,omitempty"`
}
// RolePermissionTreeResponse represents the complete role permission tree response
type RolePermissionTreeResponse struct {
Success bool `json:"success"`
Data RolePermissionData `json:"data"`
}
// RolePermissionData contains role, group, and access information
type RolePermissionData struct {
Role string `json:"role"`
// Group []string `json:"group"`
Access []*AccessTreeItem `json:"access"`
}
// AccessTreeItem represents a single item in the access tree
type AccessTreeItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
URL string `json:"url"`
Group string `json:"group"`
Level int `json:"level"`
Sort int `json:"sort"`
Active bool `json:"active"`
Children []*AccessTreeItem `json:"children,omitempty"`
Permission *PermissionResponse `json:"permission,omitempty"`
}
// RolePageAccessResponse represents the structured role page access response
type RolePageAccessResponse struct {
RoleID string `json:"role_id"`
DataRole []*AccessTreeItem `json:"dataRole"`
DataAll []*AccessTreeItem `json:"dataAll"`
}
+55
View File
@@ -0,0 +1,55 @@
package permission
import (
"time"
)
// RolPermission entity represents the rol_permission table in the database
type RolPermission struct {
Id int64 `json:"id" db:"id" gorm:"primaryKey;autoIncrement"`
Create bool `json:"create" db:"create"`
Read bool `json:"read" db:"read"`
Update bool `json:"update" db:"update"`
Disable bool `json:"disable" db:"disable"`
Delete bool `json:"delete" db:"delete"`
Active bool `json:"active" db:"active"`
FkRolPagesId int64 `json:"fk_rol_pages_id" db:"fk_rol_pages_id"`
RoleMasterName *string `json:"role_master" db:"role_master_name" gorm:"-"`
RoleKeycloak string `json:"-" db:"role_keycloak"`
GroupKeycloak string `json:"group_keycloak" db:"group_keycloak"`
CreatedAt *time.Time `json:"created_at" db:"created_at"`
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
}
// TableName specifies the table name for RolPermission
func (RolPermission) TableName() string {
return "role_access.rol_permission"
}
// RolPermissionWithPage struct untuk hasil JOIN dengan pages
type RolPermissionWithPage struct {
// Fields dari RolPermission
Id int64 `json:"id" db:"id"`
Create bool `json:"create" db:"create"`
Read bool `json:"read" db:"read"`
Update bool `json:"update" db:"update"`
Disable bool `json:"disable" db:"disable"`
Delete bool `json:"delete" db:"delete"`
Active bool `json:"active" db:"active"`
FkRolPagesId int64 `json:"fk_rol_pages_id" db:"fk_rol_pages_id"`
RoleMasterName *string `json:"role_master" db:"role_master_name" db:"-"`
RoleKeycloak string `json:"-" db:"role_keycloak"`
GroupKeycloak string `json:"group_keycloak" db:"group_keycloak"`
CreatedAt *time.Time `json:"created_at" db:"created_at"`
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
// Fields dari RolPages (hasil JOIN)
PageName string `json:"page_name" db:"page_name"`
PageUrl string `json:"page_url" db:"page_url"`
PageGroup string `json:"page_group" db:"page_group"`
PageLevel int `json:"page_level" db:"page_level"`
PageSort int `json:"page_sort" db:"page_sort"`
PageActive bool `json:"page_active" db:"page_active"`
PageIcon string `json:"page_icon" db:"page_icon"`
PageParentId *int64 `json:"page_parent" db:"page_parent"`
}
+33
View File
@@ -0,0 +1,33 @@
package permission
// mapRequestToEntity converts RolPermissionRequest to *RolPermission entity
func mapRequestToEntity(req RolPermissionRequest) *RolPermission {
return &RolPermission{
Create: req.Create,
Read: req.Read,
Update: req.Update,
Disable: req.Disable,
Delete: req.Delete,
Active: req.Active,
FkRolPagesId: req.FkRolPagesId,
RoleKeycloak: req.RoleKeycloak,
GroupKeycloak: req.GroupKeycloak,
}
}
// mapEntityToResponse converts *RolPermission entity to *RolPermissionResponse DTO
func mapEntityToResponse(e *RolPermission) *RolPermissionResponse {
return &RolPermissionResponse{
Id: e.Id,
Create: e.Create,
Read: e.Read,
Update: e.Update,
Disable: e.Disable,
Delete: e.Delete,
Active: e.Active,
FkRolPagesId: e.FkRolPagesId,
RoleMasterName: e.RoleMasterName,
RoleKeycloak: e.RoleKeycloak,
GroupKeycloak: e.GroupKeycloak,
}
}
@@ -0,0 +1,648 @@
package permission
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"service/internal/infrastructure/database"
"service/pkg/logger"
"service/pkg/utils/query"
"github.com/jmoiron/sqlx"
"gorm.io/gorm"
)
type CommandRepository interface {
Create(ctx context.Context, entity *RolPermission) error
Update(ctx context.Context, entity *RolPermission) error
Delete(ctx context.Context, id int64) error
}
type QueryRepository interface {
FindAll(ctx context.Context, limit, offset int) ([]RolPermission, int64, error)
FindByID(ctx context.Context, id int64) (*RolPermission, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RolPermission, int64, error)
FindByRoleAndGroup(ctx context.Context, roleKeycloak string, groupKeycloak []string) ([]*RolPermission, error)
FindByRoleAndPages(ctx context.Context, roleKeycloak string, pageIDs []int64) ([]*RolPermission, error)
FindPermissionsWithPages(ctx context.Context, roleKeycloak string, groupKeycloak []string, activeOnly *bool) ([]*RolPermissionWithPage, error)
FindPermissionsWithRole(ctx context.Context, roleKeycloak string, activeOnly *bool) ([]*RolPermissionWithPage, error)
FindActivePagesWithPermission(ctx context.Context, active bool) ([]*RolPermissionWithPage, error)
}
type repository struct {
dbManager database.Service
dbName string
qb query.QueryBuilder
dbType query.DBType
allowedColumnsMap map[string]bool // Cache untuk validasi kolom yang diizinkan
}
func NewCommandRepository(dbManager database.Service, dbName string) CommandRepository {
return NewRepository(dbManager, dbName)
}
func NewQueryRepository(dbManager database.Service, dbName string) QueryRepository {
return NewRepository(dbManager, dbName)
}
func NewRepository(dbManager database.Service, dbName string) *repository {
dbType := query.DBTypePostgreSQL
allowedColumns := []string{
"id",
"create",
"read",
"update",
"disable",
"delete",
"active",
"fk_rol_pages_id",
"role_keycloak",
"group_keycloak",
"created_at",
"updated_at",
"role_master_name",
}
qb := query.NewSQLQueryBuilder(dbType).
SetSecurityOptions(true, 1000).
SetQueryLogging(true).
SetQueryTimeout(30).
SetAllowedColumns(allowedColumns)
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType, allowedColumnsMap: createAllowedColumnsMap(allowedColumns)}
}
// Helper function untuk membuat map dari allowed columns
func createAllowedColumnsMap(columns []string) map[string]bool {
result := make(map[string]bool)
for _, col := range columns {
result[col] = true
}
return result
}
// isColumnAllowed memvalidasi apakah kolom diizinkan untuk digunakan
func (r *repository) isColumnAllowed(column string) bool {
return r.allowedColumnsMap[column]
}
// Helper mengambil koneksi Master dari DB Manager
func (r *repository) getWriteGormDB() (*gorm.DB, error) {
return r.dbManager.GetGormDB(r.dbName)
}
// Helper mengambil koneksi Read-Replica dari DB Manager
func (r *repository) getReadSQLXDB() (*sqlx.DB, error) {
sqlDB, err := r.dbManager.GetReadDB(r.dbName)
if err != nil {
return nil, fmt.Errorf("failed to get read db: %w", err)
}
return sqlx.NewDb(sqlDB, "pgx").Unsafe(), nil
}
// FindAll fetches all RolPermission with pagination
func (r *repository) FindAll(ctx context.Context, limit, offset int) ([]RolPermission, int64, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, 0, err
}
// Build query dengan filter active = true
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
},
Limit: limit,
Offset: offset,
Sort: []query.SortField{query.CreateDescSort("rp.created_at")},
}
var results []RolPermission
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, 0, fmt.Errorf("failed to execute find all query: %w", err)
}
count, err := r.qb.ExecuteCount(ctx, sqlxDB, dq)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute count query: %w", err)
}
return results, count, nil
}
// FindByID fetches a single RolPermission by ID
func (r *repository) FindByID(ctx context.Context, id int64) (*RolPermission, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
// Build query dengan filter ID dan active = true
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
},
Filters: []query.FilterGroup{{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rp.id", id),
},
}},
Limit: 1,
}
var result RolPermission
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, dq, &result); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("failed to fetch RolPermission: %w", err)
}
return &result, nil
}
func (r *repository) Search(ctx context.Context, filters map[string]interface{}, sorts []query.SortField, limit, offset int) ([]RolPermission, int64, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, 0, err
}
// Build dynamic filters dengan validasi kolom yang aman
var dynamicFilters []query.DynamicFilter
for k, v := range filters {
// Normalisasi nama kolom menjadi lowercase agar sesuai dengan allowedColumns
colName := strings.ToLower(k)
if !r.isColumnAllowed(colName) {
continue
}
dbCol := "rp." + colName
if colName == "role_master_name" {
dbCol = "rm.name"
}
switch val := v.(type) {
case string:
if val != "" {
if colName == "role_keycloak" || colName == "group_keycloak" || colName == "role_master_name" {
dynamicFilters = append(dynamicFilters, query.CreateFilter(dbCol, query.OpILike, "%"+val+"%"))
} else {
if boolVal, err := strconv.ParseBool(val); err == nil && (val == "true" || val == "false" || val == "1" || val == "0") {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(dbCol, boolVal))
} else if intVal, err := strconv.Atoi(val); err == nil {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(dbCol, intVal))
} else {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(dbCol, val))
}
}
}
default:
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter(dbCol, val))
}
}
// Konversi sort fields dengan validasi keamanan
var sortFields []query.SortField
for _, sort := range sorts {
if r.isColumnAllowed(sort.Column) {
dbCol := "rp." + sort.Column
if strings.ToLower(sort.Column) == "role_master_name" {
dbCol = "rm.name"
}
sortFields = append(sortFields, query.SortField{
Column: dbCol,
Order: sort.Order,
})
}
}
// Jika tidak ada sort yang valid, gunakan default
if len(sortFields) == 0 {
sortFields = []query.SortField{query.CreateDescSort("rp.created_at")}
}
// Build query dengan query builder
q := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
Limit: limit,
Offset: offset,
Sort: sortFields,
}
logger.Default().Info("Built search query", logger.String("request", fmt.Sprintf("%+v", q)))
var results []RolPermission
if err := r.qb.ExecuteQuery(ctx, sqlxDB, q, &results); err != nil {
return nil, 0, fmt.Errorf("failed to execute search query: %w", err)
}
count, err := r.qb.ExecuteCount(ctx, sqlxDB, q)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute search count query: %w", err)
}
return results, count, nil
}
func (r *repository) FindByRoleAndGroup(ctx context.Context, roleKeycloak string, groupKeycloak []string) ([]*RolPermission, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
// Build dynamic filters
var dynamicFilters []query.DynamicFilter
// Filter berdasarkan role
if roleKeycloak != "" {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter("rp.role_keycloak", roleKeycloak))
}
// Filter berdasarkan group jika disediakan
if len(groupKeycloak) > 0 {
dynamicFilters = append(dynamicFilters, query.CreateFilter("rp.group_keycloak", query.OpIn, groupKeycloak))
}
// Jika tidak ada role maupun group, return empty result
if roleKeycloak == "" && len(groupKeycloak) == 0 {
return []*RolPermission{}, nil
}
// Build query dengan query builder
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
}
var results []*RolPermission
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, fmt.Errorf("failed to execute find by role and group query: %w", err)
}
return results, nil
}
// Method baru untuk query dengan JOIN - lebih efisien
func (r *repository) FindPermissionsWithPages(ctx context.Context, roleKeycloak string, groupKeycloak []string, activeOnly *bool) ([]*RolPermissionWithPage, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
// Build dynamic filters
var dynamicFilters []query.DynamicFilter
// Filter berdasarkan role
if roleKeycloak != "" {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter("rp.role_keycloak", roleKeycloak))
}
// Filter berdasarkan group jika disediakan
if len(groupKeycloak) > 0 {
dynamicFilters = append(dynamicFilters, query.CreateFilter("rp.group_keycloak", query.OpIn, groupKeycloak))
}
// Filter active jika diminta
if activeOnly != nil {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter("rp.active", *activeOnly))
}
// Build query dengan JOIN menggunakan query builder
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
{Expression: "p.name", Alias: "page_name"},
{Expression: "p.url", Alias: "page_url"},
{Expression: "p.level", Alias: "page_level"},
{Expression: "p.sort", Alias: "page_sort"},
{Expression: "p.active", Alias: "page_active"},
{Expression: "p.icon", Alias: "page_icon"},
{Expression: "p.parent", Alias: "page_parent"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
{
Type: "LEFT",
Table: "role_access.rol_pages",
Alias: "p",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("p.id", "rp.fk_rol_pages_id"),
},
},
},
},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
Sort: []query.SortField{query.CreateAscSort("p.sort"), query.CreateAscSort("p.id")},
}
var results []*RolPermissionWithPage
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, fmt.Errorf("failed to execute permissions with pages query: %w", err)
}
return results, nil
}
// Method baru untuk query dengan JOIN - lebih efisien
func (r *repository) FindPermissionsWithRole(ctx context.Context, roleKeycloak string, activeOnly *bool) ([]*RolPermissionWithPage, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
// Build dynamic filters
var dynamicFilters []query.DynamicFilter
// Filter berdasarkan role
if roleKeycloak != "" {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter("rp.role_keycloak", roleKeycloak))
}
// Filter active jika diminta
if activeOnly != nil {
dynamicFilters = append(dynamicFilters, query.CreateEqualFilter("rp.active", *activeOnly))
}
// Build query dengan JOIN menggunakan query builder
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
{Expression: "p.name", Alias: "page_name"},
{Expression: "p.url", Alias: "page_url"},
{Expression: "p.level", Alias: "page_level"},
{Expression: "p.sort", Alias: "page_sort"},
{Expression: "p.active", Alias: "page_active"},
{Expression: "p.icon", Alias: "page_icon"},
{Expression: "p.parent", Alias: "page_parent"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
{
Type: "LEFT",
Table: "role_access.rol_pages",
Alias: "p",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("p.id", "rp.fk_rol_pages_id"),
},
},
},
},
Filters: []query.FilterGroup{{Filters: dynamicFilters}},
Sort: []query.SortField{query.CreateAscSort("p.sort"), query.CreateAscSort("p.id")},
}
var results []*RolPermissionWithPage
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, fmt.Errorf("failed to execute permissions with pages query: %w", err)
}
return results, nil
}
// FindActivePagesWithPermission menjalankan Query "dataAll"
func (r *repository) FindActivePagesWithPermission(ctx context.Context, active bool) ([]*RolPermissionWithPage, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
dq := query.DynamicQuery{
From: "role_access.rol_pages",
Aliases: "p",
Fields: []query.SelectField{
{Expression: "p.id", Alias: "fk_rol_pages_id"},
{Expression: "p.name", Alias: "page_name"},
{Expression: "p.icon", Alias: "page_icon"},
{Expression: "p.url", Alias: "page_url"},
{Expression: "p.level", Alias: "page_level"},
{Expression: "p.sort", Alias: "page_sort"},
{Expression: "p.parent", Alias: "page_parent"},
{Expression: "p.active", Alias: "page_active"},
{Expression: "p.created_at", Alias: "created_at"},
{Expression: "p.updated_at", Alias: "updated_at"},
{Expression: "COALESCE(BOOL_OR(rp.create), false)", Alias: "create"},
{Expression: "COALESCE(BOOL_OR(rp.read), false)", Alias: "read"},
{Expression: "COALESCE(BOOL_OR(rp.update), false)", Alias: "update"},
{Expression: "COALESCE(BOOL_OR(rp.disable), false)", Alias: "disable"},
{Expression: "COALESCE(BOOL_OR(rp.delete), false)", Alias: "delete"},
{Expression: "COALESCE(BOOL_OR(rp.active), false)", Alias: "active"},
},
Joins: []query.Join{
{
Type: "LEFT",
Table: "role_access.rol_permission",
Alias: "rp",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("p.id", "rp.fk_rol_pages_id"),
},
},
},
},
GroupBy: []string{
"p.id",
},
Filters: []query.FilterGroup{
{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("p.active", active),
},
},
},
Sort: []query.SortField{
query.CreateAscSort("p.id"),
},
}
var results []*RolPermissionWithPage
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, fmt.Errorf("failed to execute active pages query: %w", err)
}
return results, nil
}
func (r *repository) FindByRoleAndPages(ctx context.Context, roleOrGroupKeycloak string, pageIDs []int64) ([]*RolPermission, error) {
sqlxDB, err := r.getReadSQLXDB()
if err != nil {
return nil, err
}
// Build dynamic filters
var filterGroups []query.FilterGroup
// Filter berdasarkan role atau group (OR logic)
if roleOrGroupKeycloak != "" {
roleFilter := query.CreateEqualFilter("rp.role_keycloak", roleOrGroupKeycloak)
groupFilter := query.CreateEqualFilter("rp.group_keycloak", roleOrGroupKeycloak)
// Buat filter group dengan OR logic
filterGroups = append(filterGroups, query.FilterGroup{
Filters: []query.DynamicFilter{roleFilter, groupFilter},
LogicOp: "OR",
})
}
// Filter berdasarkan page IDs jika disediakan
if len(pageIDs) > 0 {
pageFilter := query.CreateFilter("rp.fk_rol_pages_id", query.OpIn, pageIDs)
filterGroups = append(filterGroups, query.FilterGroup{
Filters: []query.DynamicFilter{pageFilter},
})
}
// Build query dengan query builder
dq := query.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rp",
Fields: []query.SelectField{
{Expression: "rp.*"},
{Expression: "rm.name", Alias: "role_master_name"},
},
Joins: []query.Join{
{
Type: "INNER",
Table: "role_access.rol_master",
Alias: "rm",
OnConditions: query.FilterGroup{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("rm.id", "rp.role_keycloak"),
},
},
},
},
Filters: filterGroups,
}
var results []*RolPermission
if err := r.qb.ExecuteQuery(ctx, sqlxDB, dq, &results); err != nil {
return nil, fmt.Errorf("failed to execute find by role and pages query: %w", err)
}
return results, nil
}
// Create creates a new RolPermission
func (r *repository) Create(ctx context.Context, entity *RolPermission) error {
if entity == nil {
return errors.New("entity cannot be nil")
}
db, err := r.getWriteGormDB()
if err != nil {
return err
}
result := db.WithContext(ctx).Table(entity.TableName()).Create(entity)
if result.Error != nil {
return fmt.Errorf("failed to create RolPermission: %w", result.Error)
}
return nil
}
func (r *repository) Update(ctx context.Context, entity *RolPermission) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Table(entity.TableName()).Save(entity).Error
}
func (r *repository) Delete(ctx context.Context, id int64) error {
db, err := r.getWriteGormDB()
if err != nil {
return err
}
return db.WithContext(ctx).Delete(&RolPermission{}, id).Error
}
+880
View File
@@ -0,0 +1,880 @@
package permission
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"service/pkg/errors"
"service/pkg/logger"
"service/pkg/utils/query"
"strings"
"time"
"gorm.io/gorm"
// Import pages entity untuk referensi
"service/internal/infrastructure/cache"
rolePages "service/internal/master/role/pages"
)
type Service interface {
GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error)
GetDetail(ctx context.Context, id int64) (*RolPermissionResponse, error)
Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error)
Create(ctx context.Context, req RolPermissionRequest) (*RolPermissionResponse, error)
Update(ctx context.Context, id int64, req RolPermissionRequest) (*RolPermissionResponse, error)
Delete(ctx context.Context, id int64) error
GetByRoleAndGroup(ctx context.Context, roleKeycloak string, groupKeycloak []string, fkRolPagesId int64) (*RolPermissionResponse, error)
GetByRoleAndPages(ctx context.Context, roleKeycloak string, fkRolPagesIds []int64) ([]*RolPermissionResponse, error)
GetRolePermissionTree(ctx context.Context, roleKeycloak string, groupKeycloak []string, activeOnly *bool) (*RolePermissionTreeResponse, error)
GetRolePermissionTreeRole(ctx context.Context, roleKeycloak string, activeOnly *bool) (*RolePermissionTreeResponse, error)
GetRolePagesList(ctx context.Context, roleKeycloak string, limit, offset int, activeOnly *bool) (map[string]interface{}, error)
}
type service struct {
cmdRepo CommandRepository
queryRepo QueryRepository
cache *cache.Manager
}
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, pagesRepo rolePages.QueryRepository, cacheManager *cache.Manager) Service {
return &service{cmdRepo: cmdRepo, queryRepo: queryRepo, cache: cacheManager}
}
func (s *service) GetList(ctx context.Context, limit, offset int, sorts []string, activeFilter *bool) (map[string]interface{}, error) {
filters := map[string]interface{}{}
if activeFilter != nil {
filters["active"] = *activeFilter
}
// Gunakan kapabilitas method Search agar filter dynamic & cache ikut ter-handle dengan efisien
return s.Search(ctx, filters, sorts, limit, offset)
}
func (s *service) GetDetail(ctx context.Context, id int64) (*RolPermissionResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
entity, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RolPermission detail").Cause(err).Build()
}
if entity == nil {
return nil, errors.NotFoundError().Message("RolPermission not found").Metadata("id", id).Build()
}
return mapEntityToResponse(entity), nil
}
func (s *service) Search(ctx context.Context, filters map[string]interface{}, sorts []string, limit, offset int) (map[string]interface{}, error) {
if limit == 0 {
limit = 10
} else if limit < -1 || limit > 100 {
limit = 10
}
if offset < 0 {
offset = 0
}
// --- 1. Implementasi Caching (Check) ---
filterBytes, _ := json.Marshal(filters)
sortBytes, _ := json.Marshal(sorts)
// Membuat string gabungan dari filter, sort, dan pagination, lalu di-hash dengan SHA-256
hashInput := fmt.Sprintf("%s|%s|%d|%d", string(filterBytes), string(sortBytes), limit, offset)
hash := sha256.Sum256([]byte(hashInput))
cacheKey := fmt.Sprintf("role_permission_search_v2:%s", hex.EncodeToString(hash[:]))
if s.cache != nil {
var strData string
if err := s.cache.Get(ctx, cacheKey, &strData); err == nil && strData != "" {
var cachedData struct {
Data []*RolPermissionResponse `json:"data"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
if err := json.Unmarshal([]byte(strData), &cachedData); err == nil {
logger.Default().Debug("Cache hit for Role Permission Search", logger.String("key", cacheKey))
return map[string]interface{}{
"data": cachedData.Data,
"total": cachedData.Total,
"limit": cachedData.Limit,
"offset": cachedData.Offset,
}, nil
}
}
}
// Konversi sorts string ke SortField
var sortFields []query.SortField
for _, sort := range sorts {
if sort == "" {
continue
}
// Default ASC, tandai dengan - untuk DESC
order := "ASC"
column := sort
if strings.HasPrefix(sort, "-") {
order = "DESC"
column = strings.TrimPrefix(sort, "-")
} else if strings.HasPrefix(sort, "+") {
column = strings.TrimPrefix(sort, "+")
}
// Validasi kolom yang diizinkan (lowercase untuk mapping)
allowedColumns := map[string]bool{
"id": true,
"create": true,
"read": true,
"update": true,
"disable": true,
"delete": true,
"active": true,
"fk_rol_pages_id": true,
"role_keycloak": true,
"group_keycloak": true,
"created_at": true,
"role_master_name": true,
}
if allowedColumns[column] {
sortFields = append(sortFields, query.SortField{
Column: column,
Order: order,
})
}
}
// Jika tidak ada sort yang valid, gunakan default
if len(sortFields) == 0 {
sortFields = []query.SortField{query.CreateAscSort("id")}
}
entities, total, err := s.queryRepo.Search(ctx, filters, sortFields, limit, offset)
if err != nil {
return nil, errors.InternalError().Message("Failed to search RolPermissions").Cause(err).Build()
}
responses := make([]*RolPermissionResponse, len(entities))
for i, entity := range entities {
responses[i] = mapEntityToResponse(&entity)
}
responseMap := map[string]interface{}{
"data": responses, "total": total, "limit": limit, "offset": offset,
}
// --- 2. Implementasi Caching (Set) ---
if s.cache != nil {
if bytes, err := json.Marshal(responseMap); err == nil {
// TTL 5 menit untuk data search
_ = s.cache.Set(ctx, cacheKey, string(bytes), 5*time.Minute)
}
}
return responseMap, nil
}
func (s *service) Create(ctx context.Context, req RolPermissionRequest) (*RolPermissionResponse, error) {
// Validasi input
if req.FkRolPagesId <= 0 {
return nil, errors.NewValidationError().Message("Page ID is required").Metadata("fk_rol_pages_id", req.FkRolPagesId).Build()
}
if req.RoleKeycloak == "" && req.GroupKeycloak == "" {
return nil, errors.NewValidationError().Message("Role or Group must be provided").Build()
}
entity := mapRequestToEntity(req)
if err := s.cmdRepo.Create(ctx, entity); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RolPermission with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to create RolPermission").Cause(err).Build()
}
// Re-fetch to get the complete created entity
createdEntity, err := s.queryRepo.FindByID(ctx, entity.Id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve newly created RolPermission").Cause(err).Build()
}
// Invalidate cache setelah create permission baru
if s.cache != nil {
// Perhatian: Jika cache.Manager Anda tidak mendukung wildcard Delete secara native,
// Anda mungkin perlu memanggil metode s.cache.DeleteByPrefix atau menghapus key spesifik.
cacheKeyPattern := fmt.Sprintf("permission_tree:%s:*", req.RoleKeycloak)
_ = s.cache.Delete(ctx, cacheKeyPattern)
// Hapus juga cache list agar data terbaru langsung tampil
_ = s.cache.Delete(ctx, "role_permission_list:*")
// Hapus juga cache search
_ = s.cache.Delete(ctx, "role_permission_search_v2:*")
}
return mapEntityToResponse(createdEntity), nil
}
func (s *service) Update(ctx context.Context, id int64, req RolPermissionRequest) (*RolPermissionResponse, error) {
if id <= 0 {
return nil, errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah record ada
existing, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve RolPermission").Cause(err).Build()
}
if existing == nil {
return nil, errors.NotFoundError().Message("RolPermission not found").Metadata("id", id).Build()
}
// Simpan role lama untuk invalidasi cache
oldRoleKeycloak := existing.RoleKeycloak
// Update entity dengan data dari request
// existing.Create = req.Create
// existing.Read = req.Read
// existing.Update = req.Update
// existing.Disable = req.Disable
// existing.Delete = req.Delete
// existing.Active = req.Active
// existing.FkRolPagesId = req.FkRolPagesId
// existing.RoleKeycloak = req.RoleKeycloak
// existing.GroupKeycloak = req.GroupKeycloak
if err := s.cmdRepo.Update(ctx, existing); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, errors.AlreadyExistsError().Message("RolPermission with this identifier already exists").Cause(err).Build()
}
return nil, errors.InternalError().Message("Failed to update RolPermission").Cause(err).Build()
}
// Invalidate cache
if s.cache != nil {
// Invalidate cache untuk role lama dan baru jika berbeda
if oldRoleKeycloak != "" {
_ = s.cache.Delete(ctx, fmt.Sprintf("permission_tree:%s:*", oldRoleKeycloak))
}
if existing.RoleKeycloak != "" && existing.RoleKeycloak != oldRoleKeycloak {
_ = s.cache.Delete(ctx, fmt.Sprintf("permission_tree:%s:*", existing.RoleKeycloak))
}
// Invalidate cache list dan search
_ = s.cache.Delete(ctx, "role_permission_list:*")
_ = s.cache.Delete(ctx, "role_permission_search_v2:*")
}
return mapEntityToResponse(existing), nil
}
func (s *service) Delete(ctx context.Context, id int64) error {
if id <= 0 {
return errors.NewValidationError().Message("Invalid ID").Metadata("id", id).Build()
}
// Cek apakah record ada untuk mendapatkan roleKeycloak sebelum dihapus
existing, err := s.queryRepo.FindByID(ctx, id)
if err != nil {
return errors.InternalError().Message("Failed to retrieve RolPermission before deletion").Cause(err).Build()
}
if existing == nil {
// Jika sudah tidak ada, anggap operasi berhasil (idempotent)
return nil
}
if err := s.cmdRepo.Delete(ctx, id); err != nil {
return errors.InternalError().Message("Failed to delete RolPermission").Cause(err).Build()
}
// Invalidate cache
if s.cache != nil {
if existing.RoleKeycloak != "" {
_ = s.cache.Delete(ctx, fmt.Sprintf("permission_tree:%s:*", existing.RoleKeycloak))
}
// Invalidate cache list dan search
_ = s.cache.Delete(ctx, "role_permission_list:*")
_ = s.cache.Delete(ctx, "role_permission_search_v2:*")
}
return nil
}
func (s *service) GetByRoleAndGroup(ctx context.Context, roleKeycloak string, groupKeycloak []string, fkRolPagesId int64) (*RolPermissionResponse, error) {
if roleKeycloak == "" {
return nil, errors.NewValidationError().Message("Role must be provided").Build()
}
if fkRolPagesId <= 0 {
return nil, errors.NewValidationError().Message("Invalid page ID").Build()
}
entity, err := s.queryRepo.FindByRoleAndGroup(ctx, roleKeycloak, groupKeycloak)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve permission by role and group").Cause(err).Build()
}
// Cek jika tidak ada permission yang ditemukan
if len(entity) == 0 {
return nil, errors.NotFoundError().Message("Permission not found for role and group").Build()
}
// Untuk saat ini, kita kembalikan data pertama yang ditemukan.
// Anda mungkin perlu logika tambahan jika ada lebih dari satu hasil.
response := mapEntityToResponse(entity[0])
return response, nil
}
// Helper function to build page access tree
func buildPageAccessTree(pages []rolePages.RolPages, permissionMap map[int64]*RolPermission) []*PageAccess {
pageMap := make(map[int64]*PageAccess)
var roots []*PageAccess
// Convert all pages to PageAccess
for i := 0; i < len(pages); i++ {
page := &pages[i]
access := &PageAccess{
Id: page.Id,
Name: page.Name,
Icon: page.Icon,
URL: page.URL,
Level: page.Level,
Sort: page.Sort,
Active: page.Active,
}
// Add permission if exists
if perm, exists := permissionMap[page.Id]; exists {
access.Permission = &PermissionDetail{
Create: perm.Create,
Read: perm.Read,
Update: perm.Update,
Delete: perm.Delete,
Disable: perm.Disable,
}
access.Group = perm.GroupKeycloak
}
pageMap[page.Id] = access
}
// Build parent-child relationships
for i := range pages {
page := &pages[i]
if page.Parent == nil {
roots = append(roots, pageMap[page.Id])
} else {
if parent, exists := pageMap[*page.Parent]; exists {
parent.Children = append(parent.Children, pageMap[page.Id])
}
}
}
// Sort children by sort order
for _, access := range pageMap {
if len(access.Children) > 0 {
sortPageAccessChildren(access.Children)
}
}
// Sort roots by sort order
sortPageAccessChildren(roots)
return roots
}
func sortPageAccessChildren(children []*PageAccess) {
for i := 0; i < len(children); i++ {
for j := i + 1; j < len(children); j++ {
if children[i].Sort > children[j].Sort {
children[i], children[j] = children[j], children[i]
}
}
}
}
// convertPageAccessToAccessTree converts []*PageAccess to []*AccessTreeItem
func convertPageAccessToAccessTree(pageAccess []*PageAccess) []*AccessTreeItem {
result := make([]*AccessTreeItem, len(pageAccess))
for i, access := range pageAccess {
result[i] = &AccessTreeItem{
ID: access.Id,
Name: access.Name,
Icon: access.Icon,
URL: access.URL,
Group: access.Group,
Level: int(access.Level),
Sort: int(access.Sort),
Active: access.Active,
Children: convertPageAccessToAccessTree(access.Children),
}
// Convert permission if exists
if access.Permission != nil {
result[i].Permission = &PermissionResponse{
Id: access.Id,
Create: access.Permission.Create,
Read: access.Permission.Read,
Update: access.Permission.Update,
Delete: access.Permission.Delete,
Disable: access.Permission.Disable,
}
}
}
return result
}
// buildPermissionTreeHierarchy builds permission tree hierarchy from pages and permissions
func buildPermissionTreeHierarchy(pages []rolePages.RolPages, permissionMap map[int64]*RolPermission) []*PermissionAccessItem {
pageMap := make(map[int64]*PermissionAccessItem)
var roots []*PermissionAccessItem
// Convert all pages to PermissionAccessItem
for i := 0; i < len(pages); i++ {
page := &pages[i]
access := &PermissionAccessItem{
Id: page.Id,
Name: page.Name,
Icon: page.Icon,
URL: page.URL,
Level: page.Level,
Sort: page.Sort,
Active: page.Active,
}
// Add permission if exists
if perm, exists := permissionMap[page.Id]; exists {
access.Permission = &PermissionDetail{
Create: perm.Create,
Read: perm.Read,
Update: perm.Update,
Delete: perm.Delete,
Disable: perm.Disable,
}
}
pageMap[page.Id] = access
}
// Build parent-child relationships
for i := 0; i < len(pages); i++ {
page := &pages[i]
if page.Parent == nil {
roots = append(roots, pageMap[page.Id])
} else {
if parent, exists := pageMap[*page.Parent]; exists {
parent.Children = append(parent.Children, pageMap[page.Id])
}
}
}
// Sort children by sort order
for _, access := range pageMap {
if len(access.Children) > 0 {
sortPermissionAccessChildren(access.Children)
}
}
// Sort roots by sort order
sortPermissionAccessChildren(roots)
return roots
}
func sortPermissionAccessChildren(children []*PermissionAccessItem) {
for i := 0; i < len(children); i++ {
for j := i + 1; j < len(children); j++ {
if children[i].Sort > children[j].Sort {
children[i], children[j] = children[j], children[i]
}
}
}
}
func (s *service) GetByRoleAndPages(ctx context.Context, roleKeycloak string, fkRolPagesIds []int64) ([]*RolPermissionResponse, error) {
if roleKeycloak == "" {
return nil, errors.NewValidationError().Message("Role must be provided").Build()
}
if len(fkRolPagesIds) == 0 {
return nil, errors.NewValidationError().Message("Page IDs must be provided").Build()
}
entities, err := s.queryRepo.FindByRoleAndPages(ctx, roleKeycloak, fkRolPagesIds)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve permissions by role and pages").Cause(err).Build()
}
responses := make([]*RolPermissionResponse, len(entities))
for i, entity := range entities {
responses[i] = mapEntityToResponse(entity)
}
return responses, nil
}
func (s *service) GetRolePermissionTree(ctx context.Context, roleKeycloak string, groupKeycloak []string, activeOnly *bool) (*RolePermissionTreeResponse, error) {
if roleKeycloak == "" && len(groupKeycloak) == 0 {
return nil, errors.NewValidationError().Message("Role or group must be provided").Build()
}
// --- 1. Implementasi Caching (Check) ---
var activeStr string
if activeOnly != nil {
activeStr = fmt.Sprintf("%v", *activeOnly)
} else {
activeStr = "all"
}
cacheKey := fmt.Sprintf("permission_tree:%s:%s:%s", roleKeycloak, strings.Join(groupKeycloak, "_"), activeStr)
if s.cache != nil {
var strData string
if err := s.cache.Get(ctx, cacheKey, &strData); err == nil && strData != "" {
var cachedResponse RolePermissionTreeResponse
if err := json.Unmarshal([]byte(strData), &cachedResponse); err == nil {
logger.Default().Debug("Cache hit for Role Permission Tree", logger.String("key", cacheKey))
return &cachedResponse, nil
}
}
}
// Gunakan query JOIN yang lebih efisien
permissionsWithPages, err := s.queryRepo.FindPermissionsWithPages(ctx, roleKeycloak, groupKeycloak, activeOnly)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve permissions with pages").Cause(err).Build()
}
// Jika tidak ada hasil, return empty
if len(permissionsWithPages) == 0 {
return &RolePermissionTreeResponse{
Success: true,
Data: RolePermissionData{
Role: roleKeycloak,
// Group: groupKeycloak,
Access: []*AccessTreeItem{},
},
}, nil
}
// Konversi hasil JOIN ke struct pages dan permissions
var pages []rolePages.RolPages
pageMap := make(map[int64]bool)
permissionMap := make(map[int64]*RolPermission)
for _, permPage := range permissionsWithPages {
// Buat atau dapatkan page
if !pageMap[permPage.FkRolPagesId] {
page := rolePages.RolPages{
Id: permPage.FkRolPagesId,
Name: permPage.PageName,
URL: permPage.PageUrl,
Level: int16(permPage.PageLevel),
Sort: int16(permPage.PageSort),
Active: permPage.PageActive,
Icon: permPage.PageIcon,
}
if permPage.PageParentId != nil {
page.Parent = permPage.PageParentId
}
pages = append(pages, page)
pageMap[permPage.FkRolPagesId] = true
}
// Buat permission
permission := &RolPermission{
Id: permPage.Id,
Create: permPage.Create,
Read: permPage.Read,
Update: permPage.Update,
Disable: permPage.Disable,
Delete: permPage.Delete,
Active: permPage.Active,
FkRolPagesId: permPage.FkRolPagesId,
RoleKeycloak: permPage.RoleKeycloak,
GroupKeycloak: permPage.GroupKeycloak,
CreatedAt: permPage.CreatedAt,
UpdatedAt: permPage.UpdatedAt,
}
permissionMap[permPage.FkRolPagesId] = permission
}
// Build tree dengan pages yang sudah diurutkan
access := buildPageAccessTree(pages, permissionMap)
// Convert to proper response format
accessTree := convertPageAccessToAccessTree(access)
// Ambil group_keycloak yang sebenarnya dari data permission (jika ada)
var actualGroup []string
if len(permissionsWithPages) > 0 {
// Kumpulkan semua group_keycloak yang unik
groupMap := make(map[string]bool)
for _, permPage := range permissionsWithPages {
if permPage.GroupKeycloak != "" {
groupMap[permPage.GroupKeycloak] = true
}
}
// Konversi map ke slice
for group := range groupMap {
actualGroup = append(actualGroup, group)
}
}
response := &RolePermissionTreeResponse{
Success: true,
Data: RolePermissionData{
Role: roleKeycloak,
// Group: actualGroup,
Access: accessTree,
},
}
// --- 2. Implementasi Caching (Set) ---
if s.cache != nil {
if bytes, err := json.Marshal(response); err == nil {
// Simpan ke cache dengan TTL 1 jam (Dapat disesuaikan)
_ = s.cache.Set(ctx, cacheKey, string(bytes), 1*time.Hour)
}
}
return response, nil
}
func (s *service) GetRolePagesList(ctx context.Context, roleKeycloak string, limit, offset int, activeOnly *bool) (map[string]interface{}, error) {
// 1. Inisiasi Limit & Offset standar
if limit == 0 {
limit = 10
} else if limit < -1 || limit > 100 {
limit = 10
}
if offset < 0 {
offset = 0
}
// 2. Fetch dataAll: semua role pages aktif beserta permission mapping (query FindActivePagesWithPermission)
isActive := true
if activeOnly != nil {
isActive = *activeOnly
}
dataAllRecords, err := s.queryRepo.FindActivePagesWithPermission(ctx, isActive)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve active pages for dataAll").Cause(err).Build()
}
var allPages []rolePages.RolPages
allPageMap := make(map[int64]bool)
allPermissionMap := make(map[int64]*RolPermission)
for _, permPage := range dataAllRecords {
if !allPageMap[permPage.FkRolPagesId] {
page := rolePages.RolPages{
Id: permPage.FkRolPagesId,
Name: permPage.PageName,
URL: permPage.PageUrl,
Level: int16(permPage.PageLevel),
Sort: int16(permPage.PageSort),
Active: permPage.PageActive,
Icon: permPage.PageIcon,
}
if permPage.PageParentId != nil {
page.Parent = permPage.PageParentId
}
allPages = append(allPages, page)
allPageMap[permPage.FkRolPagesId] = true
}
allPermissionMap[permPage.FkRolPagesId] = &RolPermission{
Id: permPage.Id,
Create: permPage.Create,
Read: permPage.Read,
Update: permPage.Update,
Delete: permPage.Delete,
Disable: permPage.Disable,
GroupKeycloak: permPage.GroupKeycloak,
RoleKeycloak: permPage.RoleKeycloak,
}
}
accessAll := buildPageAccessTree(allPages, allPermissionMap)
accessTreeAll := convertPageAccessToAccessTree(accessAll)
// 3. Fetch seluruh role permission berserta info pages-nya (dengan filter roleKeycloak spesifik)
permissionsWithPages, err := s.queryRepo.FindPermissionsWithRole(ctx, roleKeycloak, activeOnly)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve permissions with pages").Cause(err).Build()
}
var pages []rolePages.RolPages
pageMap := make(map[int64]bool)
permissionMap := make(map[int64]*RolPermission)
for _, permPage := range permissionsWithPages {
if !pageMap[permPage.FkRolPagesId] {
page := rolePages.RolPages{
Id: permPage.FkRolPagesId,
Name: permPage.PageName,
URL: permPage.PageUrl,
Level: int16(permPage.PageLevel),
Sort: int16(permPage.PageSort),
Active: permPage.PageActive,
Icon: permPage.PageIcon,
}
if permPage.PageParentId != nil {
page.Parent = permPage.PageParentId
}
pages = append(pages, page)
pageMap[permPage.FkRolPagesId] = true
}
permissionMap[permPage.FkRolPagesId] = &RolPermission{
Id: permPage.Id,
Create: permPage.Create,
Read: permPage.Read,
Update: permPage.Update,
Delete: permPage.Delete,
Disable: permPage.Disable,
GroupKeycloak: permPage.GroupKeycloak,
RoleKeycloak: permPage.RoleKeycloak,
}
}
// Build tree menggunakan helper yang telah ada
access := buildPageAccessTree(pages, permissionMap)
accessTree := convertPageAccessToAccessTree(access)
data := []*RolePageAccessResponse{
{
RoleID: roleKeycloak,
DataRole: accessTree,
DataAll: accessTreeAll,
},
}
return map[string]interface{}{
"data": data,
"total": int64(1),
"limit": limit,
"offset": offset,
}, nil
}
func (s *service) GetRolePermissionTreeRole(ctx context.Context, roleKeycloak string, activeOnly *bool) (*RolePermissionTreeResponse, error) {
// --- 1. Implementasi Caching (Check) ---
var activeStr string
if activeOnly != nil {
activeStr = fmt.Sprintf("%v", *activeOnly)
} else {
activeStr = "all"
}
cacheKey := fmt.Sprintf("permission_tree:%s:%s", roleKeycloak, activeStr)
if s.cache != nil {
var strData string
if err := s.cache.Get(ctx, cacheKey, &strData); err == nil && strData != "" {
var cachedResponse RolePermissionTreeResponse
if err := json.Unmarshal([]byte(strData), &cachedResponse); err == nil {
logger.Default().Debug("Cache hit for Role Permission Tree", logger.String("key", cacheKey))
return &cachedResponse, nil
}
}
}
// Gunakan query JOIN yang lebih efisien
// permissionsWithPages, err := s.queryRepo.FindPermissionsWithPages(ctx, roleKeycloak, groupKeycloak, activeOnly)
permissionsWithPages, err := s.queryRepo.FindPermissionsWithRole(ctx, roleKeycloak, activeOnly)
if err != nil {
return nil, errors.InternalError().Message("Failed to retrieve permissions with pages").Cause(err).Build()
}
// Jika tidak ada hasil, return empty
if len(permissionsWithPages) == 0 {
return &RolePermissionTreeResponse{
Success: true,
Data: RolePermissionData{
Role: roleKeycloak,
// Group: groupKeycloak,
Access: []*AccessTreeItem{},
},
}, nil
}
// Konversi hasil JOIN ke struct pages dan permissions
var pages []rolePages.RolPages
pageMap := make(map[int64]bool)
permissionMap := make(map[int64]*RolPermission)
for _, permPage := range permissionsWithPages {
// Buat atau dapatkan page
if !pageMap[permPage.FkRolPagesId] {
page := rolePages.RolPages{
Id: permPage.FkRolPagesId,
Name: permPage.PageName,
URL: permPage.PageUrl,
Level: int16(permPage.PageLevel),
Sort: int16(permPage.PageSort),
Active: permPage.PageActive,
Icon: permPage.PageIcon,
}
if permPage.PageParentId != nil {
page.Parent = permPage.PageParentId
}
pages = append(pages, page)
pageMap[permPage.FkRolPagesId] = true
}
// Buat permission
permission := &RolPermission{
Id: permPage.Id,
Create: permPage.Create,
Read: permPage.Read,
Update: permPage.Update,
Disable: permPage.Disable,
Delete: permPage.Delete,
Active: permPage.Active,
FkRolPagesId: permPage.FkRolPagesId,
RoleKeycloak: permPage.RoleKeycloak,
GroupKeycloak: permPage.GroupKeycloak,
CreatedAt: permPage.CreatedAt,
UpdatedAt: permPage.UpdatedAt,
}
permissionMap[permPage.FkRolPagesId] = permission
}
// Build tree dengan pages yang sudah diurutkan
access := buildPageAccessTree(pages, permissionMap)
// Convert to proper response format
accessTree := convertPageAccessToAccessTree(access)
// Ambil group_keycloak yang sebenarnya dari data permission (jika ada)
var actualGroup []string
if len(permissionsWithPages) > 0 {
// Kumpulkan semua group_keycloak yang unik
groupMap := make(map[string]bool)
for _, permPage := range permissionsWithPages {
if permPage.GroupKeycloak != "" {
groupMap[permPage.GroupKeycloak] = true
}
}
// Konversi map ke slice
for group := range groupMap {
actualGroup = append(actualGroup, group)
}
}
response := &RolePermissionTreeResponse{
Success: true,
Data: RolePermissionData{
Role: roleKeycloak,
// Group: actualGroup,
Access: accessTree,
},
}
// --- 2. Implementasi Caching (Set) ---
if s.cache != nil {
if bytes, err := json.Marshal(response); err == nil {
// Simpan ke cache dengan TTL 1 jam (Dapat disesuaikan)
_ = s.cache.Set(ctx, cacheKey, string(bytes), 1*time.Hour)
}
}
return response, nil
}