update ignore
This commit is contained in:
No files matched your search
@@ -0,0 +1,97 @@
|
||||
package kfa
|
||||
|
||||
type KfaListResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Items struct {
|
||||
Data []KfaListItem `json:"data"`
|
||||
} `json:"items"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type KfaListItem struct {
|
||||
KfaCode string `json:"kfa_code"`
|
||||
}
|
||||
|
||||
type KfaDetailResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Result KfaDetailResult `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type KfaDetailResult struct {
|
||||
Active bool `json:"active"`
|
||||
ActiveIngredients []KfaDetailActiveIngredient `json:"active_ingredients"`
|
||||
AtcDdd interface{} `json:"atc_ddd"`
|
||||
AtcL1 interface{} `json:"atc_l1"`
|
||||
AtcL2 interface{} `json:"atc_l2"`
|
||||
AtcL3 interface{} `json:"atc_l3"`
|
||||
AtcL4 interface{} `json:"atc_l4"`
|
||||
AtcL5 interface{} `json:"atc_l5"`
|
||||
ControlledDrug interface{} `json:"controlled_drug"`
|
||||
Description *string `json:"description"`
|
||||
DosageForm interface{} `json:"dosage_form"`
|
||||
DosageUsage interface{} `json:"dosage_usage"`
|
||||
DosePerUnit *float64 `json:"dose_per_unit"`
|
||||
FarmalkesHscode *string `json:"farmalkes_hscode"`
|
||||
FarmalkesType interface{} `json:"farmalkes_type"`
|
||||
FixPrice *float64 `json:"fix_price"`
|
||||
Generik *bool `json:"generik"`
|
||||
HetPrice *float64 `json:"het_price"`
|
||||
IdentifierIds interface{} `json:"identifier_ids"`
|
||||
Image *string `json:"image"`
|
||||
Indication *string `json:"indication"`
|
||||
KfaCode string `json:"kfa_code"`
|
||||
KlasifikasiIzin *string `json:"klasifikasi_izin"`
|
||||
KodeLkpp *string `json:"kode_lkpp"`
|
||||
Manufacturer *string `json:"manufacturer"`
|
||||
NamaDagang *string `json:"nama_dagang"`
|
||||
Name string `json:"name"`
|
||||
NetWeight *float64 `json:"net_weight"`
|
||||
NetWeightUomName *string `json:"net_weight_uom_name"`
|
||||
Nie *string `json:"nie"`
|
||||
PackagingIds []KfaDetailPackaging `json:"packaging_ids"`
|
||||
ProductTemplate interface{} `json:"product_template"`
|
||||
ProduksiBuatan *string `json:"produksi_buatan"`
|
||||
Registrar *string `json:"registrar"`
|
||||
Replacement interface{} `json:"replacement"`
|
||||
RutePemberian interface{} `json:"rute_pemberian"`
|
||||
Rxterm *int `json:"rxterm"`
|
||||
ScoreBmp *float64 `json:"score_bmp"`
|
||||
ScoreTkdn *float64 `json:"score_tkdn"`
|
||||
ScoreTkdnBmp *float64 `json:"score_tkdn_bmp"`
|
||||
SideEffect *string `json:"side_effect"`
|
||||
State *string `json:"state"`
|
||||
Tags interface{} `json:"tags"`
|
||||
TayangLkpp *bool `json:"tayang_lkpp"`
|
||||
Ucum interface{} `json:"ucum"`
|
||||
Uom interface{} `json:"uom"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
Volume *float64 `json:"volume"`
|
||||
VolumeUomName *string `json:"volume_uom_name"`
|
||||
Warning *string `json:"warning"`
|
||||
Fornas interface{} `json:"fornas"`
|
||||
}
|
||||
|
||||
type KfaDetailActiveIngredient struct {
|
||||
Active bool `json:"active"`
|
||||
KekuatanZatAktif string `json:"kekuatan_zat_aktif"`
|
||||
KfaCode string `json:"kfa_code"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
ZatAktif string `json:"zat_aktif"`
|
||||
}
|
||||
|
||||
type KfaDetailPackaging struct {
|
||||
KfaCode string `json:"kfa_code"`
|
||||
Name string `json:"name"`
|
||||
PackPrice *float64 `json:"pack_price"`
|
||||
Qty int `json:"qty"`
|
||||
UomId string `json:"uom_id"`
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package kfa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KfaProduct struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
KfaCode string `gorm:"type:varchar(20);uniqueIndex;not null"`
|
||||
Name string `gorm:"type:varchar(150);not null"`
|
||||
DisplayName *string `gorm:"type:text"`
|
||||
TradeName *string `gorm:"type:varchar(150)"`
|
||||
State *string `gorm:"type:varchar(20)"`
|
||||
IsGeneric *bool `gorm:"default:false"`
|
||||
DosageForm json.RawMessage `gorm:"type:jsonb"`
|
||||
FarmalkesType json.RawMessage `gorm:"type:jsonb"`
|
||||
Route json.RawMessage `gorm:"type:jsonb"`
|
||||
ControlledDrug json.RawMessage `gorm:"type:jsonb"`
|
||||
Uom json.RawMessage `gorm:"type:jsonb"`
|
||||
FixPrice *float64 `gorm:"type:numeric(12,2)"`
|
||||
HetPrice *float64 `gorm:"type:numeric(12,2)"`
|
||||
DosePerUnit *float64 `gorm:"type:numeric(10,2)"`
|
||||
NetWeight *float64 `gorm:"type:numeric(10,2)"`
|
||||
NetWeightUom *string `gorm:"type:varchar(20)"`
|
||||
Volume *float64 `gorm:"type:numeric(10,2)"`
|
||||
VolumeUom *string `gorm:"type:varchar(20)"`
|
||||
ScoreTkdn *float64 `gorm:"type:numeric(5,2)"`
|
||||
ScoreTkdnBmp *float64 `gorm:"type:numeric(5,2)"`
|
||||
ScoreBmp *float64 `gorm:"type:numeric(5,2)"`
|
||||
Rxterm *int `gorm:"type:int4"`
|
||||
Manufacturer *string `gorm:"type:varchar(150)"`
|
||||
Registrar *string `gorm:"type:varchar(150)"`
|
||||
Nie *string `gorm:"type:varchar(100)"`
|
||||
Hscode *string `gorm:"type:varchar(100)"`
|
||||
LkppCode *string `gorm:"type:varchar(100)"`
|
||||
IsLkppActive *bool `gorm:"default:true"`
|
||||
Description *string `gorm:"type:text"`
|
||||
Indication *string `gorm:"type:text"`
|
||||
SideEffect *string `gorm:"type:text"`
|
||||
Warning *string `gorm:"type:text"`
|
||||
Image *string `gorm:"type:varchar(100)"`
|
||||
DrugClassification *string `gorm:"type:varchar(100)"`
|
||||
ManufacturingOrigin *string `gorm:"type:varchar(100)"`
|
||||
AtcInfo json.RawMessage `gorm:"type:jsonb"`
|
||||
Fornas json.RawMessage `gorm:"type:jsonb"`
|
||||
DosageUsage json.RawMessage `gorm:"type:jsonb"`
|
||||
Tags json.RawMessage `gorm:"type:jsonb"`
|
||||
IdentifierIds json.RawMessage `gorm:"type:jsonb"`
|
||||
ProductTemplate json.RawMessage `gorm:"type:jsonb"`
|
||||
Replacement json.RawMessage `gorm:"type:jsonb"`
|
||||
Ucum json.RawMessage `gorm:"type:jsonb"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
Active *bool `gorm:"default:true"`
|
||||
}
|
||||
|
||||
func (KfaProduct) TableName() string {
|
||||
return "master.kfa_products"
|
||||
}
|
||||
|
||||
type KfaProductActiveIngredient struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
ProductKfaCode string `gorm:"type:varchar(20);index"`
|
||||
ProductDisplay string `gorm:"type:text"`
|
||||
SubstanceCode string `gorm:"type:varchar(20)"`
|
||||
SubstanceName string `gorm:"type:varchar(150)"`
|
||||
StrengthValue *float64 `gorm:"type:numeric(10,2)"`
|
||||
StrengthUnit string `gorm:"type:varchar(20)"`
|
||||
StrengthText string `gorm:"type:varchar(100)"`
|
||||
State string `gorm:"type:varchar(20)"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
Active *bool `gorm:"default:true"`
|
||||
}
|
||||
|
||||
func (KfaProductActiveIngredient) TableName() string {
|
||||
return "master.kfa_product_active_ingredients"
|
||||
}
|
||||
|
||||
type KfaProductPackaging struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
ProductKfaCode string `gorm:"type:varchar(20);index"`
|
||||
ProductDisplay string `gorm:"type:text"`
|
||||
Code string `gorm:"type:varchar(20)"`
|
||||
Name string `gorm:"type:varchar(150)"`
|
||||
Quantity int `gorm:"type:int4"`
|
||||
Unit string `gorm:"type:varchar(50)"`
|
||||
Price *float64 `gorm:"type:numeric(12,2)"`
|
||||
State string `gorm:"type:varchar(20)"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
Active *bool `gorm:"default:true"`
|
||||
}
|
||||
|
||||
func (KfaProductPackaging) TableName() string {
|
||||
return "master.kfa_product_packagings"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,168 @@
|
||||
package kfa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MapKfaDetailToEntity(detail KfaDetailResult) (*KfaProduct, []KfaProductActiveIngredient, []KfaProductPackaging) {
|
||||
now := time.Now()
|
||||
|
||||
var displayName *string
|
||||
// Attempt to extract display_name safely if product template exists
|
||||
templateB := toJsonb(detail.ProductTemplate)
|
||||
if templateB != nil {
|
||||
var tpl struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
if err := json.Unmarshal(templateB, &tpl); err == nil && tpl.DisplayName != "" {
|
||||
displayName = &tpl.DisplayName
|
||||
}
|
||||
}
|
||||
if displayName == nil {
|
||||
displayName = &detail.Name
|
||||
}
|
||||
|
||||
product := &KfaProduct{
|
||||
KfaCode: detail.KfaCode,
|
||||
Name: detail.Name,
|
||||
DisplayName: displayName,
|
||||
TradeName: detail.NamaDagang,
|
||||
State: detail.State,
|
||||
IsGeneric: detail.Generik,
|
||||
DosageForm: toJsonb(detail.DosageForm),
|
||||
FarmalkesType: toJsonb(detail.FarmalkesType),
|
||||
Route: toJsonb(detail.RutePemberian),
|
||||
ControlledDrug: toJsonb(detail.ControlledDrug),
|
||||
Uom: toJsonb(detail.Uom),
|
||||
FixPrice: detail.FixPrice,
|
||||
HetPrice: detail.HetPrice,
|
||||
DosePerUnit: detail.DosePerUnit,
|
||||
NetWeight: detail.NetWeight,
|
||||
NetWeightUom: detail.NetWeightUomName,
|
||||
Volume: detail.Volume,
|
||||
VolumeUom: detail.VolumeUomName,
|
||||
ScoreTkdn: detail.ScoreTkdn,
|
||||
ScoreTkdnBmp: detail.ScoreTkdnBmp,
|
||||
ScoreBmp: detail.ScoreBmp,
|
||||
Rxterm: detail.Rxterm,
|
||||
Manufacturer: detail.Manufacturer,
|
||||
Registrar: detail.Registrar,
|
||||
Nie: detail.Nie,
|
||||
Hscode: detail.FarmalkesHscode,
|
||||
LkppCode: detail.KodeLkpp,
|
||||
IsLkppActive: detail.TayangLkpp,
|
||||
Description: detail.Description,
|
||||
Indication: detail.Indication,
|
||||
SideEffect: detail.SideEffect,
|
||||
Warning: detail.Warning,
|
||||
Image: detail.Image,
|
||||
DrugClassification: detail.KlasifikasiIzin,
|
||||
ManufacturingOrigin: detail.ProduksiBuatan,
|
||||
AtcInfo: buildAtcInfo(detail),
|
||||
Fornas: toJsonb(detail.Fornas),
|
||||
DosageUsage: toJsonb(detail.DosageUsage),
|
||||
Tags: toJsonb(detail.Tags),
|
||||
IdentifierIds: toJsonb(detail.IdentifierIds),
|
||||
ProductTemplate: templateB,
|
||||
Replacement: toJsonb(detail.Replacement),
|
||||
Ucum: toJsonb(detail.Ucum),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: parseTime(detail.UpdatedAt, now),
|
||||
Active: &detail.Active,
|
||||
}
|
||||
|
||||
var ingredients []KfaProductActiveIngredient
|
||||
for _, ing := range detail.ActiveIngredients {
|
||||
val, unit := parseStrength(ing.KekuatanZatAktif)
|
||||
ingredients = append(ingredients, KfaProductActiveIngredient{
|
||||
ProductKfaCode: product.KfaCode,
|
||||
ProductDisplay: product.Name,
|
||||
SubstanceCode: ing.KfaCode,
|
||||
SubstanceName: ing.ZatAktif,
|
||||
StrengthValue: val,
|
||||
StrengthUnit: unit,
|
||||
StrengthText: ing.KekuatanZatAktif,
|
||||
State: ing.State,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: parseTime(ing.UpdatedAt, now),
|
||||
Active: &ing.Active,
|
||||
})
|
||||
}
|
||||
|
||||
var packagings []KfaProductPackaging
|
||||
for _, pkg := range detail.PackagingIds {
|
||||
active := true
|
||||
packagings = append(packagings, KfaProductPackaging{
|
||||
ProductKfaCode: product.KfaCode,
|
||||
ProductDisplay: product.Name,
|
||||
Code: pkg.KfaCode,
|
||||
Name: pkg.Name,
|
||||
Quantity: pkg.Qty,
|
||||
Unit: pkg.UomId,
|
||||
Price: pkg.PackPrice,
|
||||
State: "valid",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Active: &active,
|
||||
})
|
||||
}
|
||||
|
||||
return product, ingredients, packagings
|
||||
}
|
||||
|
||||
func toJsonb(v interface{}) json.RawMessage {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil || string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func buildAtcInfo(d KfaDetailResult) json.RawMessage {
|
||||
m := map[string]interface{}{
|
||||
"atc_ddd": d.AtcDdd,
|
||||
"atc_l1": d.AtcL1,
|
||||
"atc_l2": d.AtcL2,
|
||||
"atc_l3": d.AtcL3,
|
||||
"atc_l4": d.AtcL4,
|
||||
"atc_l5": d.AtcL5,
|
||||
}
|
||||
return toJsonb(m)
|
||||
}
|
||||
|
||||
func parseStrength(text string) (*float64, string) {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, ""
|
||||
}
|
||||
parts := strings.Split(text, " ")
|
||||
if len(parts) == 0 {
|
||||
return nil, ""
|
||||
}
|
||||
val, err := strconv.ParseFloat(parts[0], 64)
|
||||
if err != nil {
|
||||
return nil, text
|
||||
}
|
||||
var unit string
|
||||
if len(parts) > 1 {
|
||||
unit = strings.Join(parts[1:], " ")
|
||||
}
|
||||
return &val, unit
|
||||
}
|
||||
|
||||
func parseTime(t *string, defaultTime time.Time) time.Time {
|
||||
if t == nil || *t == "" {
|
||||
return defaultTime
|
||||
}
|
||||
parsed, err := time.Parse("2006-01-02 15:04:05", *t)
|
||||
if err != nil {
|
||||
return defaultTime
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package kfa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
type TokenManager interface {
|
||||
GetAccessToken() string
|
||||
ForceRefreshAndGetToken(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
InternalBaseURL string
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
cfg Config
|
||||
repo CommandRepository
|
||||
tokenManager TokenManager
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config, repo CommandRepository, tokenManager TokenManager) *Worker {
|
||||
if cfg.PageSize == 0 {
|
||||
cfg.PageSize = 100
|
||||
}
|
||||
return &Worker{
|
||||
cfg: cfg,
|
||||
repo: repo,
|
||||
tokenManager: tokenManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) readTracker() int {
|
||||
b, err := os.ReadFile("internal/master/kfa/kfa_tracker.txt")
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
p, err := strconv.Atoi(strings.TrimSpace(string(b)))
|
||||
if err != nil || p < 1 {
|
||||
return 1
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (w *Worker) writeTracker(page int) {
|
||||
os.WriteFile("internal/master/kfa/kfa_tracker.txt", []byte(strconv.Itoa(page)), 0644)
|
||||
}
|
||||
|
||||
func (w *Worker) Run(ctx context.Context) {
|
||||
page := w.readTracker()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Default().Info("KFA Master Puller stopped")
|
||||
return
|
||||
default:
|
||||
err := w.processPage(ctx, page)
|
||||
if err != nil {
|
||||
logger.Default().Error("KFA Master Puller error processing page", logger.ErrorField(err), logger.Int("page", page))
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
page++
|
||||
w.writeTracker(page)
|
||||
time.Sleep(5 * time.Second) // Jedah halus antar halaman
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) processPage(ctx context.Context, page int) error {
|
||||
listURL := fmt.Sprintf("%s/satusehat/reference/kfa/products?page=%d&size=%d&product_type=farmasi", strings.TrimRight(w.cfg.InternalBaseURL, "/"), page, w.cfg.PageSize)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, listURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+w.tokenManager.GetAccessToken())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
resp.Body.Close()
|
||||
logger.Default().Warn("KFA Master Puller list API 401, refreshing token...", logger.Int("page", page))
|
||||
newToken, err := w.tokenManager.ForceRefreshAndGetToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, listURL, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+newToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
logger.Default().Warn("[KFA WORKER] ⏸️ Rate limit (429) pada List API, jeda 30s & ulangi halaman", logger.Int("page", page))
|
||||
return fmt.Errorf("rate limited on list API") // Akan memicu waktu tidur 30 detik di method Run() dan mencoba halaman yang sama
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("list API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var listRes KfaListResponse
|
||||
if err := json.Unmarshal(body, &listRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(listRes.Data.Items.Data) == 0 {
|
||||
logger.Default().Info("No more KFA products found, resetting to page 1 and sleeping", logger.Int("page", page))
|
||||
w.writeTracker(1)
|
||||
time.Sleep(6 * time.Hour)
|
||||
return fmt.Errorf("empty page, reset triggered")
|
||||
}
|
||||
|
||||
logger.Default().Info("Pulling KFA items", logger.Int("page", page), logger.Int("items", len(listRes.Data.Items.Data)))
|
||||
for _, item := range listRes.Data.Items.Data {
|
||||
for {
|
||||
rateLimited, err := w.fetchAndSaveDetail(ctx, item.KfaCode)
|
||||
if rateLimited {
|
||||
logger.Default().Warn("[KFA WORKER] ⏸️ Rate limit (429) dari Satu Sehat - jeda 60 detik dan retry dokumen yang sama", logger.String("kfa_code", item.KfaCode))
|
||||
time.Sleep(60 * time.Second)
|
||||
continue // Ulangi request untuk item ini (Tidak melewatkan ID)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Default().Error("Failed to fetch/save KFA detail", logger.String("kfa_code", item.KfaCode), logger.ErrorField(err))
|
||||
}
|
||||
break // Lanjut ke item/ID berikutnya bila sukses atau gagal karena alasan non-ratelimit
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond) // Pencegahan Rate Limit Part 3
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worker) fetchAndSaveDetail(ctx context.Context, kfaCode string) (bool, error) {
|
||||
detailURL := fmt.Sprintf("%s/satusehat/reference/kfa/products/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), kfaCode)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, detailURL, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+w.tokenManager.GetAccessToken())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
resp.Body.Close()
|
||||
newToken, err := w.tokenManager.ForceRefreshAndGetToken(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, detailURL, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+newToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
return true, fmt.Errorf("rate limited (429)")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("detail API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var detailRes KfaDetailResponse
|
||||
if err := json.Unmarshal(body, &detailRes); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
product, ingredients, packagings := MapKfaDetailToEntity(detailRes.Data.Result)
|
||||
return false, w.repo.UpsertProductBundle(ctx, product, ingredients, packagings)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package kfa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type CommandRepository interface {
|
||||
UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) error
|
||||
}
|
||||
|
||||
type commandRepository struct {
|
||||
dbManager database.Service
|
||||
connName string
|
||||
}
|
||||
|
||||
func NewCommandRepository(dbManager database.Service, connName string) CommandRepository {
|
||||
return &commandRepository{
|
||||
dbManager: dbManager,
|
||||
connName: connName,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *commandRepository) UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) (err error) {
|
||||
dbGorm, err := r.dbManager.GetGormDB(r.connName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqldb, err := dbGorm.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbx := sqlx.NewDb(sqldb, "postgres")
|
||||
|
||||
// Inisialisasi Query Builder (Nonaktifkan limit sekuritas untuk internal worker)
|
||||
sqlBuilder := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetQueryLogging(false).
|
||||
SetSecurityOptions(false, 0)
|
||||
|
||||
// Ekstrak semua kolom yang akan digunakan untuk mendaftarkannya ke whitelist Query Builder
|
||||
productData, productUpdateCols := extractInsertData(product, "kfa_code")
|
||||
var allowedCols []string
|
||||
allowedCols = append(allowedCols, productData.Columns...)
|
||||
if len(ingredients) > 0 {
|
||||
ingData, _ := extractInsertData(ingredients[0], "")
|
||||
allowedCols = append(allowedCols, ingData.Columns...)
|
||||
}
|
||||
if len(packagings) > 0 {
|
||||
pkgData, _ := extractInsertData(packagings[0], "")
|
||||
allowedCols = append(allowedCols, pkgData.Columns...)
|
||||
}
|
||||
allowedCols = append(allowedCols, "product_kfa_code", "kfa_code") // pastikan key relasi / conflict dimasukkan
|
||||
|
||||
// Daftarkan kolom agar terhindar dari error 'disallowed column'
|
||||
sqlBuilder.SetAllowedColumns(allowedCols)
|
||||
|
||||
tx, err := dbx.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tangani Rollback jika terjadi kegagalan atau panic
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
tx.Rollback()
|
||||
panic(p)
|
||||
} else if err != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. Upsert Induk Product (Manual Check & Update/Insert to avoid PG Constraint Error 42P10)
|
||||
var count int
|
||||
err = tx.GetContext(ctx, &count, "SELECT count(*) FROM master.kfa_products WHERE kfa_code = $1", product.KfaCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
updateData := query.UpdateData{
|
||||
Columns: productUpdateCols,
|
||||
Values: make([]interface{}, len(productUpdateCols)),
|
||||
}
|
||||
for i, col := range productUpdateCols {
|
||||
for j, insertCol := range productData.Columns {
|
||||
if col == insertCol {
|
||||
updateData.Values[i] = productData.Values[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err = sqlBuilder.ExecuteUpdate(ctx, tx, "master.kfa_products", updateData, []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{query.CreateEqualFilter("kfa_code", product.KfaCode)},
|
||||
}})
|
||||
} else {
|
||||
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_products", productData)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Hapus Active Ingredients Lama
|
||||
delFilters := []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateEqualFilter("product_kfa_code", product.KfaCode),
|
||||
},
|
||||
}}
|
||||
_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_active_ingredients", delFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Insert Active Ingredients Baru
|
||||
for _, ing := range ingredients {
|
||||
ingData, _ := extractInsertData(ing, "")
|
||||
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_active_ingredients", ingData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Hapus Packagings Lama
|
||||
_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_packagings", delFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Insert Packagings Baru
|
||||
for _, pkg := range packagings {
|
||||
pkgData, _ := extractInsertData(pkg, "")
|
||||
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_packagings", pkgData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
}
|
||||
|
||||
// Helper: Ekstrak data model struct menjadi InsertData format untuk QueryBuilder
|
||||
func extractInsertData(obj interface{}, conflictCol string) (query.InsertData, []string) {
|
||||
val := reflect.ValueOf(obj)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
typ := val.Type()
|
||||
|
||||
var cols []string
|
||||
var vals []interface{}
|
||||
var updateCols []string
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if field.PkgPath != "" { // Skip properti yang tidak diekspor
|
||||
continue
|
||||
}
|
||||
|
||||
colName := toSnakeCase(field.Name)
|
||||
if colName == "id" { // ID selalu di-skip pada operasi insert karena dia auto increment
|
||||
continue
|
||||
}
|
||||
|
||||
fieldVal := val.Field(i).Interface()
|
||||
cols = append(cols, colName)
|
||||
|
||||
if b, ok := fieldVal.(json.RawMessage); ok {
|
||||
if len(b) == 0 || string(b) == "null" {
|
||||
vals = append(vals, nil)
|
||||
} else {
|
||||
vals = append(vals, string(b))
|
||||
}
|
||||
} else {
|
||||
vals = append(vals, fieldVal)
|
||||
}
|
||||
|
||||
// Cek apa saja kolom yang valid di-update saat konflik (mengecualikan relasi/kode referensi)
|
||||
if colName != conflictCol && colName != "created_at" && colName != "product_kfa_code" {
|
||||
updateCols = append(updateCols, colName)
|
||||
}
|
||||
}
|
||||
|
||||
return query.InsertData{
|
||||
Columns: cols,
|
||||
Values: vals,
|
||||
}, updateCols
|
||||
}
|
||||
|
||||
// Helper: Ubah PascalCase field Go ke mode snake_case untuk tabel database
|
||||
func toSnakeCase(s string) string {
|
||||
if s == "ID" {
|
||||
return "id"
|
||||
}
|
||||
var result strings.Builder
|
||||
var prevUpper bool
|
||||
for i, r := range s {
|
||||
if unicode.IsUpper(r) {
|
||||
if i > 0 && !prevUpper {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
prevUpper = true
|
||||
} else {
|
||||
result.WriteRune(r)
|
||||
prevUpper = false
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user