Files
satusehat-worker/internal/master/role/pages/service.go
T
2026-04-14 01:21:54 +00:00

309 lines
9.4 KiB
Go

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
}