first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package auth
|
||||
|
||||
// RefreshTokenRequest mendefinisikan struktur untuk request refresh token.
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
// Provider membantu membedakan antara "jwt" dan "keycloak".
|
||||
// Ini membuat endpoint menjadi fleksibel.
|
||||
Provider string `json:"provider" binding:"required,oneof=jwt keycloak"`
|
||||
}
|
||||
|
||||
// LoginResponse adalah respons standar untuk login dan refresh token.
|
||||
type LoginResponse struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
IdToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
RoleID int64 `json:"role_id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Id int64 `json:"id" db:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Password string `json:"-" db:"password"` // Hidden dari response JSON
|
||||
RoleID *int64 `json:"role_id" db:"role_id"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt *time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommandRepository interface {
|
||||
CreateUser(ctx context.Context, user *User) error
|
||||
UpdateUser(ctx context.Context, user *User) error
|
||||
ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) error
|
||||
RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error
|
||||
}
|
||||
|
||||
type QueryRepository interface {
|
||||
FindUserByEmail(ctx context.Context, email string) (*User, error)
|
||||
FindUserByID(ctx context.Context, id int64) (*User, error)
|
||||
FindRefreshToken(ctx context.Context, token string) (*StoredToken, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
dbName string
|
||||
qb query.QueryBuilder
|
||||
dbType query.DBType
|
||||
}
|
||||
|
||||
func newRepository(dbManager database.Service, dbName string) *repository {
|
||||
dbType := query.DBTypePostgreSQL
|
||||
allowedColumns := []string{
|
||||
"Id",
|
||||
"Email",
|
||||
"Password",
|
||||
"RoleID",
|
||||
"Active",
|
||||
"CreatedAt",
|
||||
"UpdatedAt",
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(dbType).
|
||||
SetSecurityOptions(true, 1000).
|
||||
SetQueryLogging(true).
|
||||
SetQueryTimeout(30).
|
||||
SetAllowedColumns(allowedColumns)
|
||||
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType}
|
||||
}
|
||||
|
||||
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 (r *repository) getWriteGormDB() (*gorm.DB, error) {
|
||||
return r.dbManager.GetGormDB(r.dbName)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r *repository) getWriteSQLXDB() (*sqlx.DB, error) {
|
||||
// Mengambil koneksi database utama (tulis)
|
||||
sqlDB, err := r.dbManager.GetDB(r.dbName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get write db: %w", err)
|
||||
}
|
||||
return sqlx.NewDb(sqlDB, "pgx"), nil
|
||||
}
|
||||
|
||||
func (r *repository) CreateUser(ctx context.Context, user *User) error {
|
||||
db, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateUser(ctx context.Context, user *User) error {
|
||||
db, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (r *repository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
q := query.DynamicQuery{
|
||||
From: "users", // GORM default table name for User struct
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateEqualFilter("Email", email),
|
||||
},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil // Not found is not an error, service layer will handle it
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *repository) ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) (err error) {
|
||||
// 1. Dapatkan koneksi database tulis
|
||||
writeDB, err := r.getWriteSQLXDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Mulai transaksi
|
||||
tx, err := writeDB.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
// 3. Defer Rollback. Ini akan dieksekusi jika fungsi return error atau panic.
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
tx.Rollback()
|
||||
panic(p) // Lanjutkan panic setelah rollback
|
||||
} else if err != nil {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
err = fmt.Errorf("transaction error: %v, rollback error: %v", err, rbErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 4. Operasi 1: Update password pengguna
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"Password", "UpdatedAt"},
|
||||
Values: []interface{}{newPassword, time.Now()},
|
||||
}
|
||||
filters := []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{query.CreateEqualFilter("Id", userID)},
|
||||
}}
|
||||
|
||||
// Berikan objek transaksi 'tx' ke Query Builder
|
||||
if _, err = r.qb.ExecuteUpdate(ctx, tx, "users", updateData, filters); err != nil {
|
||||
return fmt.Errorf("failed to update password in tx: %w", err)
|
||||
}
|
||||
|
||||
// 5. Operasi 2: Catat aktivitas
|
||||
logData := query.InsertData{
|
||||
Columns: []string{"user_id", "activity", "created_at"},
|
||||
Values: []interface{}{userID, "password changed via transaction", time.Now()},
|
||||
}
|
||||
|
||||
// Gunakan objek transaksi 'tx' yang sama
|
||||
if _, err = r.qb.ExecuteInsert(ctx, tx, "user_activity_logs", logData); err != nil {
|
||||
// Asumsikan tabel 'user_activity_logs' ada
|
||||
return fmt.Errorf("failed to log activity in tx: %w", err)
|
||||
}
|
||||
|
||||
// 6. Jika semua berhasil, commit transaksi
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *repository) FindUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
q := query.DynamicQuery{
|
||||
From: "users", // GORM default table name for User struct
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateEqualFilter("Id", id),
|
||||
},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil // Not found is not an error, service layer will handle it
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find user by id: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindRefreshToken(ctx context.Context, token string) (*StoredToken, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stored StoredToken
|
||||
q := query.DynamicQuery{
|
||||
From: "refresh_tokens",
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{query.CreateEqualFilter("Token", token)},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &stored); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find refresh token: %w", err)
|
||||
}
|
||||
return &stored, nil
|
||||
}
|
||||
|
||||
func (r *repository) RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error {
|
||||
// Implementasi sederhana rotasi token
|
||||
writeDB, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Sesuaikan dengan struktur tabel Refresh Token yang Anda miliki di database
|
||||
// Ini adalah stub untuk memenuhi kontrak interface
|
||||
return writeDB.WithContext(ctx).Exec("UPDATE refresh_tokens SET is_revoked = ? WHERE token = ?", true, oldToken).Error
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type StoredToken struct {
|
||||
Token string
|
||||
IsRevoked bool
|
||||
UserID int64
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
func (t *StoredToken) IsExpired() bool { return false /* implementasi */ }
|
||||
|
||||
// Service mendefinisikan interface untuk operasi otentikasi.
|
||||
type Service interface {
|
||||
RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error)
|
||||
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
||||
Register(ctx context.Context, req RegisterRequest) (interface{}, error)
|
||||
Logout(ctx context.Context, token string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
cmdRepo CommandRepository
|
||||
queryRepo QueryRepository
|
||||
cache *cache.Manager
|
||||
config *config.Config // Ganti keycloakClient dengan config
|
||||
}
|
||||
|
||||
// NewService membuat instance service auth baru.
|
||||
// Signature diperbarui untuk menerima config secara keseluruhan.
|
||||
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, cache *cache.Manager, cfg *config.Config) Service {
|
||||
return &service{
|
||||
cmdRepo: cmdRepo,
|
||||
queryRepo: queryRepo,
|
||||
cache: cache,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Login mengimplementasikan otentikasi menggunakan data dummy untuk keperluan testing.
|
||||
func (s *service) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||
// Simulasi pengecekan kredensial dengan beberapa akun dummy
|
||||
type dummyUser struct {
|
||||
Password string
|
||||
UserID string
|
||||
RoleID string
|
||||
Name string
|
||||
}
|
||||
|
||||
dummyUsers := map[string]dummyUser{
|
||||
"[email protected]": {"password123", "1001", "admin", "Admin Dummy"},
|
||||
"[email protected]": {"password123", "1002", "user", "User Dummy"},
|
||||
"[email protected]": {"password123", "1003", "manager", "Manager Dummy"},
|
||||
}
|
||||
|
||||
user, exists := dummyUsers[req.Email]
|
||||
if !exists || user.Password != req.Password {
|
||||
return nil, errors.UnauthorizedError().Message("Email atau password salah. Coba: [email protected], [email protected], [email protected] (password123)").Build()
|
||||
}
|
||||
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "fallback_secret_key_change_in_production"
|
||||
}
|
||||
|
||||
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.UserID,
|
||||
"email": req.Email,
|
||||
"role_id": user.RoleID,
|
||||
"name": user.Name,
|
||||
"exp": time.Now().Add(60 * time.Minute).Unix(),
|
||||
})
|
||||
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||
|
||||
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.UserID,
|
||||
"email": req.Email,
|
||||
"role_id": user.RoleID,
|
||||
"name": user.Name,
|
||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
newRefreshTokenStr, _ := refreshToken.SignedString([]byte(secret))
|
||||
|
||||
return &LoginResponse{
|
||||
Provider: "jwt",
|
||||
AccessToken: newAccessTokenStr,
|
||||
RefreshToken: newRefreshTokenStr,
|
||||
ExpiresIn: 900, // 15 menit dalam detik
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register mengimplementasikan pendaftaran user dengan respons data json dummy.
|
||||
func (s *service) Register(ctx context.Context, req RegisterRequest) (interface{}, error) {
|
||||
// Mengembalikan response dummy seolah-olah user berhasil dibuat di DB
|
||||
return map[string]interface{}{
|
||||
"id": 1002, // Generated Dummy ID
|
||||
"name": req.Name,
|
||||
"email": req.Email,
|
||||
"role_id": req.RoleID,
|
||||
"created_at": time.Now().Format(time.RFC3339),
|
||||
"status": "active",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout mengimplementasikan proses logout dengan memasukkan token ke cache blacklist.
|
||||
func (s *service) Logout(ctx context.Context, token string) error {
|
||||
if token == "" {
|
||||
return errors.NewValidationError().Message("Token tidak valid").Build()
|
||||
}
|
||||
|
||||
// Invalidate token di cache (Blacklist) jika cache manager aktif
|
||||
if s.cache != nil {
|
||||
// Set token ke dalam blacklist dengan durasi 15 menit (Sesuai umur access token)
|
||||
_ = s.cache.Set(ctx, "blacklist_token:"+token, true, 15*time.Minute)
|
||||
}
|
||||
|
||||
// TODO: Segera hapus/tandai Refresh Token sebagai revoked di database agar tidak bisa
|
||||
// digunakan lagi untuk generate access token baru setelah pengguna berhasil logout.
|
||||
// _ = s.cmdRepo.RevokeAllTokensByToken(ctx, token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshToken menangani refresh token untuk berbagai provider.
|
||||
func (s *service) RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error) {
|
||||
switch req.Provider {
|
||||
case "jwt":
|
||||
return s.refreshInternalJWT(ctx, req.RefreshToken)
|
||||
case "keycloak":
|
||||
return s.refreshKeycloakToken(ctx, req.RefreshToken)
|
||||
|
||||
default:
|
||||
return nil, errors.NewValidationError().Message("Provider tidak didukung").Metadata("provider", req.Provider).Build()
|
||||
}
|
||||
}
|
||||
|
||||
// refreshInternalJWT menangani validasi dan pembuatan JWT internal baru.
|
||||
func (s *service) refreshInternalJWT(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "fallback_secret_key_change_in_production"
|
||||
}
|
||||
|
||||
// Parse dan validasi refresh token
|
||||
parsedToken, err := jwt.Parse(refreshToken, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method")
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !parsedToken.Valid {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau telah kedaluwarsa").Build()
|
||||
}
|
||||
|
||||
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.UnauthorizedError().Message("Klaim token tidak valid").Build()
|
||||
}
|
||||
|
||||
// Cek apakah ini dummy user dari fungsi Login. Jika ya, lewati pengecekan DB.
|
||||
isDummyUser := false
|
||||
if userID, ok := claims["user_id"].(string); ok && (userID == "1001" || userID == "1002" || userID == "1003") {
|
||||
isDummyUser = true
|
||||
}
|
||||
|
||||
// Hanya lakukan pengecekan ke database jika bukan dummy user
|
||||
if !isDummyUser {
|
||||
existingToken, err := s.queryRepo.FindRefreshToken(ctx, refreshToken)
|
||||
if err != nil {
|
||||
// Kesalahan saat query ke database
|
||||
return nil, errors.InternalError().Message("Gagal memvalidasi refresh token").Cause(err).Build()
|
||||
}
|
||||
if existingToken == nil {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau tidak ditemukan").Build()
|
||||
}
|
||||
if existingToken.IsRevoked {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token telah dicabut (dibatalkan)").Metadata("reason", "revoked").Build()
|
||||
}
|
||||
}
|
||||
|
||||
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": claims["user_id"],
|
||||
"email": claims["email"],
|
||||
"role_id": claims["role_id"],
|
||||
"name": claims["name"],
|
||||
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||
})
|
||||
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||
|
||||
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||
newRefreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": claims["user_id"],
|
||||
"email": claims["email"],
|
||||
"role_id": claims["role_id"],
|
||||
"name": claims["name"],
|
||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
newRefreshTokenStr, _ := newRefreshToken.SignedString([]byte(secret))
|
||||
|
||||
// Revoke token lama dan simpan yang baru (hanya untuk user non-dummy)
|
||||
if !isDummyUser {
|
||||
_ = s.cmdRepo.RevokeAndSaveTokens(ctx, refreshToken, newRefreshTokenStr)
|
||||
}
|
||||
|
||||
return &LoginResponse{
|
||||
Provider: "jwt",
|
||||
AccessToken: newAccessTokenStr,
|
||||
RefreshToken: newRefreshTokenStr,
|
||||
ExpiresIn: 900, // 15 menit dalam detik
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refreshKeycloakToken menangani refresh token melalui Keycloak.
|
||||
func (s *service) refreshKeycloakToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||
if !s.config.Keycloak.Enabled || s.config.Keycloak.URL == "" {
|
||||
return nil, errors.InternalError().Message("Provider Keycloak tidak dikonfigurasi dengan benar").Build()
|
||||
}
|
||||
|
||||
tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", s.config.Keycloak.URL, s.config.Keycloak.Realm)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("client_id", s.config.Keycloak.ClientID)
|
||||
data.Set("client_secret", s.config.Keycloak.ClientSecret)
|
||||
data.Set("refresh_token", refreshToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal membuat request ke Keycloak").Build()
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal berkomunikasi dengan Keycloak").Build()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var keycloakErr map[string]interface{}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&keycloakErr)
|
||||
return nil, errors.UnauthorizedError().
|
||||
Message("Gagal me-refresh token dengan Keycloak").
|
||||
Metadata("upstream_status", resp.StatusCode).
|
||||
Metadata("upstream_error", keycloakErr).Build()
|
||||
}
|
||||
|
||||
var tokenResponse LoginResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal mem-parsing respons Keycloak").Build()
|
||||
}
|
||||
|
||||
// Tambahkan provider ke response
|
||||
tokenResponse.Provider = "keycloak"
|
||||
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package antrol
|
||||
|
||||
// Poli merepresentasikan data referensi poli dari API Antrean RS BPJS
|
||||
type Poli struct {
|
||||
KdPoli string `json:"kdpoli"`
|
||||
NmPoli string `json:"nmpoli"`
|
||||
KdSubSpesialis string `json:"kdsubspesialis"`
|
||||
NmSubSpesialis string `json:"nmsubspesialis"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package antrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetRefPoli(ctx context.Context) ([]Poli, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
// GetRefPoli fetches referensi poli from BPJS API
|
||||
//
|
||||
// # This function will return an array of Poli and error if any
|
||||
//
|
||||
// Context is used to pass the request context to the underlying
|
||||
// client
|
||||
//
|
||||
// The function will return an error if the request to BPJS API
|
||||
// fails or if the response cannot be unmarshalled into an array
|
||||
// of Poli
|
||||
//
|
||||
// The function will return an empty array and nil error if the request
|
||||
// to BPJS API succeeds but the response is an empty array
|
||||
func (r *repository) GetRefPoli(ctx context.Context) ([]Poli, error) {
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", "ref/poli", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []Poli
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal ref poli antrol response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package antrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetRefPoli(ctx context.Context) ([]Poli, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetRefPoli(ctx context.Context) ([]Poli, error) {
|
||||
res, err := s.repo.GetRefPoli(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil referensi poli Antrean RS BPJS").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package bed
|
||||
|
||||
// BedData merepresentasikan payload untuk membuat, mengubah, atau mengambil data ketersediaan Bed
|
||||
type BedData struct {
|
||||
KodeKelas string `json:"kodekelas"`
|
||||
KodeRuang string `json:"koderuang"`
|
||||
NamaRuang string `json:"namaruang"`
|
||||
Kapasitas int `json:"kapasitas"`
|
||||
Tersedia int `json:"tersedia"`
|
||||
TersediaPria int `json:"tersediapria"`
|
||||
TersediaWanita int `json:"tersediawanita"`
|
||||
TersediaPriaWanita int `json:"tersediapriawanita"`
|
||||
}
|
||||
|
||||
// BedDeletePayload merepresentasikan payload untuk menghapus data Bed
|
||||
type BedDeletePayload struct {
|
||||
KodeKelas string `json:"kodekelas"`
|
||||
KodeRuang string `json:"koderuang"`
|
||||
}
|
||||
|
||||
// BedReadResponse merepresentasikan balikan dari endpoint GET Read Bed
|
||||
type BedReadResponse struct {
|
||||
List []BedData `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package bed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Read(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error)
|
||||
Create(ctx context.Context, kdPpk string, payload BedData) error
|
||||
Update(ctx context.Context, kdPpk string, payload BedData) error
|
||||
Delete(ctx context.Context, kdPpk string, payload BedDeletePayload) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
func (r *repository) Read(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error) {
|
||||
endpoint := fmt.Sprintf("rest/bed/read/%s/%d/%d", kdPpk, start, limit)
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result BedReadResponse
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal applicare read response: %w", err)
|
||||
}
|
||||
|
||||
return result.List, nil
|
||||
}
|
||||
|
||||
func (r *repository) Create(ctx context.Context, kdPpk string, payload BedData) error {
|
||||
endpoint := fmt.Sprintf("rest/bed/create/%s", kdPpk)
|
||||
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||
// Aplicares biasanya mengembalikan sukses di metadata yang sudah dihandle oleh client.go
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) Update(ctx context.Context, kdPpk string, payload BedData) error {
|
||||
// Endpoint Update Aplicares menggunakan method POST, bukan PUT
|
||||
endpoint := fmt.Sprintf("rest/bed/update/%s", kdPpk)
|
||||
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) Delete(ctx context.Context, kdPpk string, payload BedDeletePayload) error {
|
||||
// Endpoint Delete Aplicares juga menggunakan method POST
|
||||
endpoint := fmt.Sprintf("rest/bed/delete/%s", kdPpk)
|
||||
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package bed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetBedList(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error)
|
||||
CreateBed(ctx context.Context, kdPpk string, req BedData) error
|
||||
UpdateBed(ctx context.Context, kdPpk string, req BedData) error
|
||||
DeleteBed(ctx context.Context, kdPpk string, req BedDeletePayload) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetBedList(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error) {
|
||||
if kdPpk == "" {
|
||||
return nil, errors.NewValidationError().Message("Kode PPK (Kode Faskes) wajib diisi").Build()
|
||||
}
|
||||
|
||||
res, err := s.repo.Read(ctx, kdPpk, start, limit)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil data ketersediaan tempat tidur (Aplicares)").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *service) CreateBed(ctx context.Context, kdPpk string, req BedData) error {
|
||||
err := s.repo.Create(ctx, kdPpk, req)
|
||||
if err != nil {
|
||||
return errors.ExternalError().Message("Gagal membuat data tempat tidur baru (Aplicares)").Cause(err).Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) UpdateBed(ctx context.Context, kdPpk string, req BedData) error {
|
||||
err := s.repo.Update(ctx, kdPpk, req)
|
||||
if err != nil {
|
||||
return errors.ExternalError().Message("Gagal memperbarui data tempat tidur (Aplicares)").Cause(err).Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteBed(ctx context.Context, kdPpk string, req BedDeletePayload) error {
|
||||
err := s.repo.Delete(ctx, kdPpk, req)
|
||||
if err != nil {
|
||||
return errors.ExternalError().Message("Gagal menghapus data tempat tidur (Aplicares)").Cause(err).Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dpho
|
||||
|
||||
// DPHOData merepresentasikan detail referensi DPHO dari API Apotek
|
||||
type DPHOData struct {
|
||||
KodeObat string `json:"kodeobat"`
|
||||
NamaObat string `json:"namaobat"`
|
||||
PRB string `json:"prb"`
|
||||
Kronis string `json:"kronis"`
|
||||
Kemo string `json:"kemo"`
|
||||
Harga string `json:"harga"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dpho
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetDPHO(ctx context.Context) ([]DPHOData, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
func (r *repository) GetDPHO(ctx context.Context) ([]DPHOData, error) {
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", "referensi/dpho", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Coba parsing ke dalam format object { "list": [...] }
|
||||
var result struct {
|
||||
List []DPHOData `json:"list"`
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
// Fallback jika API mengembalikan array langsung [...]
|
||||
var directList []DPHOData
|
||||
if err2 := json.Unmarshal(respBytes, &directList); err2 == nil {
|
||||
return directList, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to unmarshal apotek dpho response: %w", err)
|
||||
}
|
||||
return result.List, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dpho
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetDPHO(ctx context.Context) ([]DPHOData, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetDPHO(ctx context.Context) ([]DPHOData, error) {
|
||||
res, err := s.repo.GetDPHO(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil referensi DPHO Apotek BPJS").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package poli
|
||||
|
||||
// PoliData merepresentasikan detail referensi Poli dari API Apotek
|
||||
type PoliData struct {
|
||||
KodePoli string `json:"kodepoli"`
|
||||
NamaPoli string `json:"namapoli"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package poli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetPoli(ctx context.Context, param string) ([]PoliData, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
func (r *repository) GetPoli(ctx context.Context, param string) ([]PoliData, error) {
|
||||
endpoint := fmt.Sprintf("referensi/poli/%s", param)
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
List []PoliData `json:"list"`
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
var directList []PoliData
|
||||
if err2 := json.Unmarshal(respBytes, &directList); err2 == nil {
|
||||
return directList, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to unmarshal apotek poli response: %w", err)
|
||||
}
|
||||
return result.List, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package poli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetPoli(ctx context.Context, param string) ([]PoliData, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetPoli(ctx context.Context, param string) ([]PoliData, error) {
|
||||
if param == "" {
|
||||
return nil, errors.NewValidationError().Message("Parameter pencarian Poli wajib diisi").Build()
|
||||
}
|
||||
res, err := s.repo.GetPoli(ctx, param)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil referensi Poli Apotek BPJS").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package peserta
|
||||
|
||||
// PesertaResponse merepresentasikan wrapper data dari API BPJS VClaim
|
||||
type PesertaResponse struct {
|
||||
Peserta PesertaData `json:"peserta"`
|
||||
}
|
||||
|
||||
// PesertaData merepresentasikan entitas detail Peserta BPJS
|
||||
type PesertaData struct {
|
||||
NoKartu string `json:"noKartu"`
|
||||
Nik string `json:"nik"`
|
||||
Nama string `json:"nama"`
|
||||
Pisa string `json:"pisa"`
|
||||
Sex string `json:"sex"`
|
||||
Umur Umur `json:"umur"`
|
||||
TglLahir string `json:"tglLahir"`
|
||||
StatusPeserta StatusPeserta `json:"statusPeserta"`
|
||||
ProviderUmum ProviderUmum `json:"provUmum"`
|
||||
JenisPeserta JenisPeserta `json:"jenisPeserta"`
|
||||
HakKelas HakKelas `json:"hakKelas"`
|
||||
Informasi Informasi `json:"informasi"`
|
||||
}
|
||||
|
||||
type Umur struct {
|
||||
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
|
||||
UmurSekarang string `json:"umurSekarang"`
|
||||
}
|
||||
|
||||
type StatusPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
}
|
||||
|
||||
type ProviderUmum struct {
|
||||
KdProvider string `json:"kdProvider"`
|
||||
NmProvider string `json:"nmProvider"`
|
||||
}
|
||||
|
||||
type JenisPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
}
|
||||
|
||||
type HakKelas struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
}
|
||||
|
||||
type Informasi struct {
|
||||
Dinsos string `json:"dinsos"`
|
||||
ProlanisPRB string `json:"prolanisPRB"`
|
||||
NoSKTM string `json:"noSKTM"`
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package peserta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error)
|
||||
GetByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
func (r *repository) GetByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error) {
|
||||
endpoint := fmt.Sprintf("Peserta/nokartu/%s/tglSEP/%s", noKartu, tglSEP)
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result PesertaResponse
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal peserta response: %w", err)
|
||||
}
|
||||
|
||||
return &result.Peserta, nil
|
||||
}
|
||||
|
||||
func (r *repository) GetByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error) {
|
||||
endpoint := fmt.Sprintf("Peserta/nik/%s/tglSEP/%s", nik, tglSEP)
|
||||
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result PesertaResponse
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal peserta response: %w", err)
|
||||
}
|
||||
|
||||
return &result.Peserta, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package peserta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/pkg/errors"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetPesertaByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error)
|
||||
GetPesertaByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) GetPesertaByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error) {
|
||||
if noKartu == "" || tglSEP == "" {
|
||||
return nil, errors.NewValidationError().Message("Nomor Kartu dan Tanggal SEP wajib diisi").Build()
|
||||
}
|
||||
|
||||
res, err := s.repo.GetByNoKartu(ctx, noKartu, tglSEP)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil data peserta BPJS berdasarkan No Kartu").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *service) GetPesertaByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error) {
|
||||
if nik == "" || tglSEP == "" {
|
||||
return nil, errors.NewValidationError().Message("NIK dan Tanggal SEP wajib diisi").Build()
|
||||
}
|
||||
|
||||
res, err := s.repo.GetByNIK(ctx, nik, tglSEP)
|
||||
if err != nil {
|
||||
return nil, errors.ExternalError().Message("Gagal mengambil data peserta BPJS berdasarkan NIK").Cause(err).Build()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package sep
|
||||
|
||||
// CreateSEPRequest adalah data payload untuk insert pembuatan SEP.
|
||||
// Strukturnya mengikuti format yang dibutuhkan oleh API VClaim 2.0.
|
||||
type CreateSEPRequest struct {
|
||||
Request struct {
|
||||
Sep `json:"t_sep"`
|
||||
} `json:"request"`
|
||||
}
|
||||
|
||||
// Sep adalah detail data untuk pembuatan SEP.
|
||||
type Sep struct {
|
||||
NoKartu string `json:"noKartu"`
|
||||
TglSep string `json:"tglSep"`
|
||||
PpkPelayanan string `json:"ppkPelayanan"`
|
||||
JnsPelayanan string `json:"jnsPelayanan"`
|
||||
KlsRawat KlsRawat `json:"klsRawat"`
|
||||
NoMR string `json:"noMR"`
|
||||
Rujukan Rujukan `json:"rujukan"`
|
||||
Catatan string `json:"catatan"`
|
||||
DiagAwal string `json:"diagAwal"`
|
||||
Poli Poli `json:"poli"`
|
||||
Cob string `json:"cob"`
|
||||
Katarak string `json:"katarak"`
|
||||
Jaminan Jaminan `json:"jaminan"`
|
||||
Tujuan string `json:"tujuan"`
|
||||
FlagProcedure string `json:"flagProcedure"`
|
||||
KdPenunjang string `json:"kdPenunjang"`
|
||||
AssesmentPel string `json:"assesmentPel"`
|
||||
NoSurat string `json:"noSurat"`
|
||||
KodeDPJP string `json:"kodeDPJP"`
|
||||
DpjpLayan string `json:"dpjpLayan"`
|
||||
NoTelp string `json:"noTelp"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
type KlsRawat struct {
|
||||
KlsRawatHak string `json:"klsRawatHak"`
|
||||
KlsRawatNaik string `json:"klsRawatNaik,omitempty"`
|
||||
Pembiayaan string `json:"pembiayaan,omitempty"`
|
||||
PenanggungJawab string `json:"penanggungJawab,omitempty"`
|
||||
}
|
||||
|
||||
type Rujukan struct {
|
||||
AsalRujukan string `json:"asalRujukan"`
|
||||
TglRujukan string `json:"tglRujukan"`
|
||||
NoRujukan string `json:"noRujukan"`
|
||||
PpkRujukan string `json:"ppkRujukan"`
|
||||
}
|
||||
|
||||
type Poli struct {
|
||||
Tujuan string `json:"tujuan"`
|
||||
Eksekutif string `json:"eksekutif"`
|
||||
}
|
||||
|
||||
type Jaminan struct {
|
||||
LakaLantas string `json:"lakaLantas"`
|
||||
NoLP string `json:"noLP,omitempty"`
|
||||
Penjamin Penjamin `json:"penjamin,omitempty"`
|
||||
}
|
||||
|
||||
type Penjamin struct {
|
||||
TglKejadian string `json:"tglKejadian,omitempty"`
|
||||
Keterangan string `json:"keterangan,omitempty"`
|
||||
Suplesi struct {
|
||||
Suplesi string `json:"suplesi"`
|
||||
NoSepSuplesi string `json:"noSepSuplesi,omitempty"`
|
||||
LokasiLaka struct {
|
||||
KdPropinsi string `json:"kdPropinsi"`
|
||||
KdKabupaten string `json:"kdKabupaten"`
|
||||
KdKecamatan string `json:"kdKecamatan"`
|
||||
} `json:"lokasiLaka,omitempty"`
|
||||
} `json:"suplesi,omitempty"`
|
||||
}
|
||||
|
||||
// CreateSEPResponse adalah data balikan setelah SEP berhasil dicreate.
|
||||
type CreateSEPResponse struct {
|
||||
Sep struct {
|
||||
NoSep string `json:"noSep"`
|
||||
TglSep string `json:"tglSep"`
|
||||
Poli string `json:"poli"`
|
||||
Diagnosa string `json:"diagnosa"`
|
||||
Catatan string `json:"catatan"`
|
||||
JnsRawat string `json:"jnsRawat"`
|
||||
KlsRawat string `json:"klsRawat"`
|
||||
Penjamin string `json:"penjamin"`
|
||||
} `json:"sep"`
|
||||
}
|
||||
|
||||
// UpdateSEPRequest adalah data payload untuk update SEP.
|
||||
type UpdateSEPRequest struct {
|
||||
Request struct {
|
||||
Sep struct {
|
||||
NoSep string `json:"noSep"`
|
||||
KlsRawat KlsRawat `json:"klsRawat"`
|
||||
NoMR string `json:"noMR"`
|
||||
Catatan string `json:"catatan"`
|
||||
DiagAwal string `json:"diagAwal"`
|
||||
Poli Poli `json:"poli"`
|
||||
Eksekutif string `json:"eksekutif"`
|
||||
Cob string `json:"cob"`
|
||||
Katarak string `json:"katarak"`
|
||||
Jaminan Jaminan `json:"jaminan"`
|
||||
NoTelp string `json:"noTelp"`
|
||||
User string `json:"user"`
|
||||
} `json:"t_sep"`
|
||||
} `json:"request"`
|
||||
}
|
||||
|
||||
// DeleteSEPRequest adalah data payload untuk menghapus SEP.
|
||||
type DeleteSEPRequest struct {
|
||||
Request struct {
|
||||
Sep struct {
|
||||
NoSep string `json:"noSep"`
|
||||
User string `json:"user"`
|
||||
} `json:"t_sep"`
|
||||
} `json:"request"`
|
||||
}
|
||||
|
||||
// SEPDetailResponse adalah data balikan untuk detail SEP.
|
||||
type SEPDetailResponse struct {
|
||||
Catatan string `json:"catatan"`
|
||||
Diagnosa string `json:"diagnosa"`
|
||||
JnsPelayanan string `json:"jnsPelayanan"`
|
||||
KelasRawat string `json:"kelasRawat"`
|
||||
NoRujukan string `json:"noRujukan"`
|
||||
NoSep string `json:"noSep"`
|
||||
Penjamin string `json:"penjamin"`
|
||||
Peserta struct {
|
||||
Nama string `json:"nama"`
|
||||
NoKartu string `json:"noKartu"`
|
||||
NoMr string `json:"noMr"`
|
||||
} `json:"peserta"`
|
||||
Poli string `json:"poli"`
|
||||
TglSep string `json:"tglSep"`
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package sep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"service/internal/interfaces/bpjs"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
CreateSEP(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error)
|
||||
UpdateSEP(ctx context.Context, req UpdateSEPRequest) (string, error)
|
||||
DeleteSEP(ctx context.Context, req DeleteSEPRequest) (string, error)
|
||||
GetSEP(ctx context.Context, noSEP string) (*SEPDetailResponse, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client bpjs.BpjsClient
|
||||
}
|
||||
|
||||
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||
return &repository{client: client}
|
||||
}
|
||||
|
||||
func (r *repository) CreateSEP(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error) {
|
||||
endpoint := "SEP/2.0/insert"
|
||||
decryptedBytes, err := r.client.DoRequest(ctx, "POST", endpoint, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result CreateSEPResponse
|
||||
return &result, json.Unmarshal(decryptedBytes, &result)
|
||||
}
|
||||
|
||||
func (r *repository) UpdateSEP(ctx context.Context, req UpdateSEPRequest) (string, error) {
|
||||
endpoint := "SEP/2.0/update"
|
||||
decryptedBytes, err := r.client.DoRequest(ctx, "PUT", endpoint, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Response dari update SEP biasanya hanya string nomor SEP itu sendiri
|
||||
return string(decryptedBytes), nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteSEP(ctx context.Context, req DeleteSEPRequest) (string, error) {
|
||||
endpoint := "SEP/2.0/delete"
|
||||
// BPJS API untuk delete menggunakan POST method dengan payload spesifik
|
||||
decryptedBytes, err := r.client.DoRequest(ctx, "POST", endpoint, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Response dari delete SEP biasanya hanya string "OK" atau pesan konfirmasi
|
||||
return string(decryptedBytes), nil
|
||||
}
|
||||
|
||||
func (r *repository) GetSEP(ctx context.Context, noSEP string) (*SEPDetailResponse, error) {
|
||||
endpoint := fmt.Sprintf("SEP/%s", noSEP)
|
||||
decryptedBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result SEPDetailResponse
|
||||
if err := json.Unmarshal(decryptedBytes, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal SEP detail response: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sep
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Create(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error)
|
||||
Update(ctx context.Context, req UpdateSEPRequest) (string, error)
|
||||
Delete(ctx context.Context, req DeleteSEPRequest) (string, error)
|
||||
GetDetail(ctx context.Context, noSEP string) (*SEPDetailResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) Service {
|
||||
return &service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *service) Create(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error) {
|
||||
// Anda bisa menambahkan validasi atau logika bisnis di sini sebelum memanggil repository.
|
||||
return s.repo.CreateSEP(ctx, req)
|
||||
}
|
||||
|
||||
func (s *service) Update(ctx context.Context, req UpdateSEPRequest) (string, error) {
|
||||
// TODO: Tambahkan validasi untuk request update di sini.
|
||||
// Contoh: if req.Request.Sep.NoSep == "" { return "", errors.New("nomor SEP wajib diisi") }
|
||||
return s.repo.UpdateSEP(ctx, req)
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, req DeleteSEPRequest) (string, error) {
|
||||
// TODO: Tambahkan validasi untuk request delete di sini.
|
||||
return s.repo.DeleteSEP(ctx, req)
|
||||
}
|
||||
|
||||
func (s *service) GetDetail(ctx context.Context, noSEP string) (*SEPDetailResponse, error) {
|
||||
// TODO: Tambahkan validasi untuk noSEP di sini.
|
||||
return s.repo.GetSEP(ctx, noSEP)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/cache.go
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache interface defines the contract for cache implementations
|
||||
type Cache interface {
|
||||
// Basic operations
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
Exists(ctx context.Context, key string) (bool, error)
|
||||
|
||||
// Struct operations
|
||||
GetStruct(ctx context.Context, key string, dest interface{}) error
|
||||
SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error
|
||||
|
||||
// Numeric operations
|
||||
Increment(ctx context.Context, key string, delta int64) (int64, error)
|
||||
Decrement(ctx context.Context, key string, delta int64) (int64, error)
|
||||
|
||||
// Utility operations
|
||||
TTL(ctx context.Context, key string) (time.Duration, error)
|
||||
Close() error
|
||||
Health(ctx context.Context) error
|
||||
|
||||
// Batch operations
|
||||
MGet(ctx context.Context, keys ...string) ([]interface{}, error)
|
||||
MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error
|
||||
}
|
||||
|
||||
// CacheConfig holds configuration for cache
|
||||
type CacheConfig struct {
|
||||
Enabled bool
|
||||
DefaultTTL time.Duration
|
||||
SessionTTL time.Duration
|
||||
RateLimitTTL time.Duration
|
||||
CleanupInterval time.Duration
|
||||
MaxRetries int
|
||||
RetryDelay time.Duration
|
||||
Redis RedisConfig
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
// Common cache key prefixes
|
||||
const (
|
||||
KeyPrefixSession = "session:"
|
||||
KeyPrefixUser = "user:"
|
||||
KeyPrefixToken = "token:"
|
||||
KeyPrefixRateLimit = "rate_limit:"
|
||||
KeyPrefixCache = "cache:"
|
||||
KeyPrefixTemp = "temp:"
|
||||
)
|
||||
|
||||
// Common expiration times
|
||||
const (
|
||||
ExpirationSession = 24 * time.Hour
|
||||
ExpirationShort = 5 * time.Minute
|
||||
ExpirationMedium = 30 * time.Minute
|
||||
ExpirationLong = 2 * time.Hour
|
||||
ExpirationVeryLong = 24 * time.Hour
|
||||
)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/factory.go
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"service/internal/infrastructure/config"
|
||||
)
|
||||
|
||||
// Factory creates cache instances based on configuration
|
||||
type Factory struct {
|
||||
config config.CacheConfig
|
||||
}
|
||||
|
||||
// NewFactory creates a new cache factory
|
||||
func NewFactory(cfg config.CacheConfig) *Factory {
|
||||
return &Factory{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a cache instance based on configuration
|
||||
func (f *Factory) Create() (Cache, error) {
|
||||
if !f.config.Enabled {
|
||||
return NewNoOpCache(), nil
|
||||
}
|
||||
|
||||
// For now, only Redis is supported
|
||||
return NewRedisCache(CacheConfig{
|
||||
Enabled: f.config.Enabled,
|
||||
DefaultTTL: f.config.DefaultTTL,
|
||||
SessionTTL: f.config.SessionTTL,
|
||||
RateLimitTTL: f.config.RateLimitTTL,
|
||||
CleanupInterval: f.config.CleanupInterval,
|
||||
MaxRetries: f.config.MaxRetries,
|
||||
RetryDelay: f.config.RetryDelay,
|
||||
Redis: RedisConfig{
|
||||
Host: f.config.Redis.Host,
|
||||
Port: f.config.Redis.Port,
|
||||
Password: f.config.Redis.Password,
|
||||
DB: f.config.Redis.DB,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateManager creates a cache manager with the configured cache
|
||||
func (f *Factory) CreateManager() (*Manager, error) {
|
||||
cache, err := f.Create()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cache: %w", err)
|
||||
}
|
||||
|
||||
return NewManager(cache, CacheConfig{
|
||||
Enabled: f.config.Enabled,
|
||||
DefaultTTL: f.config.DefaultTTL,
|
||||
SessionTTL: f.config.SessionTTL,
|
||||
RateLimitTTL: f.config.RateLimitTTL,
|
||||
CleanupInterval: f.config.CleanupInterval,
|
||||
MaxRetries: f.config.MaxRetries,
|
||||
RetryDelay: f.config.RetryDelay,
|
||||
Redis: RedisConfig{
|
||||
Host: f.config.Redis.Host,
|
||||
Port: f.config.Redis.Port,
|
||||
Password: f.config.Redis.Password,
|
||||
DB: f.config.Redis.DB,
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager provides high-level cache operations
|
||||
type Manager struct {
|
||||
cache Cache
|
||||
config CacheConfig
|
||||
}
|
||||
|
||||
// RedisClientProvider interface untuk cache yang menyediakan Redis client
|
||||
type RedisClientProvider interface {
|
||||
GetRedisClient() interface{}
|
||||
}
|
||||
|
||||
// NewManager creates a new cache manager
|
||||
func NewManager(cache Cache, config CacheConfig) *Manager {
|
||||
return &Manager{
|
||||
cache: cache,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Session operations
|
||||
func (m *Manager) SetSession(ctx context.Context, sessionID string, userData interface{}) error {
|
||||
key := KeyPrefixSession + sessionID
|
||||
return m.cache.SetStruct(ctx, key, userData, m.config.SessionTTL)
|
||||
}
|
||||
|
||||
func (m *Manager) GetSession(ctx context.Context, sessionID string, dest interface{}) error {
|
||||
key := KeyPrefixSession + sessionID
|
||||
return m.cache.GetStruct(ctx, key, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteSession(ctx context.Context, sessionID string) error {
|
||||
key := KeyPrefixSession + sessionID
|
||||
return m.cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Rate limiting operations
|
||||
func (m *Manager) IncrementRateLimit(ctx context.Context, identifier string) (int64, error) {
|
||||
key := KeyPrefixRateLimit + identifier
|
||||
return m.cache.Increment(ctx, key, 1)
|
||||
}
|
||||
|
||||
func (m *Manager) GetRateLimit(ctx context.Context, identifier string) (int64, error) {
|
||||
key := KeyPrefixRateLimit + identifier
|
||||
// Try to get as string first, then convert
|
||||
value, err := m.cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int64
|
||||
_, err = fmt.Sscanf(value, "%d", &count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (m *Manager) ResetRateLimit(ctx context.Context, identifier string) error {
|
||||
key := KeyPrefixRateLimit + identifier
|
||||
return m.cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
func (m *Manager) SetRateLimit(ctx context.Context, identifier string, count int64) error {
|
||||
key := KeyPrefixRateLimit + identifier
|
||||
value := fmt.Sprintf("%d", count)
|
||||
return m.cache.Set(ctx, key, value, m.config.RateLimitTTL)
|
||||
}
|
||||
|
||||
// User cache operations
|
||||
func (m *Manager) SetUser(ctx context.Context, userID string, userData interface{}) error {
|
||||
key := KeyPrefixUser + userID
|
||||
return m.cache.SetStruct(ctx, key, userData, m.config.DefaultTTL)
|
||||
}
|
||||
|
||||
func (m *Manager) GetUser(ctx context.Context, userID string, dest interface{}) error {
|
||||
key := KeyPrefixUser + userID
|
||||
return m.cache.GetStruct(ctx, key, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteUser(ctx context.Context, userID string) error {
|
||||
key := KeyPrefixUser + userID
|
||||
return m.cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Token operations
|
||||
func (m *Manager) SetToken(ctx context.Context, token string, tokenData interface{}, expiration time.Duration) error {
|
||||
key := KeyPrefixToken + token
|
||||
return m.cache.SetStruct(ctx, key, tokenData, expiration)
|
||||
}
|
||||
|
||||
func (m *Manager) GetToken(ctx context.Context, token string, dest interface{}) error {
|
||||
key := KeyPrefixToken + token
|
||||
return m.cache.GetStruct(ctx, key, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteToken(ctx context.Context, token string) error {
|
||||
key := KeyPrefixToken + token
|
||||
return m.cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// Generic cache operations with TTL
|
||||
func (m *Manager) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
return m.cache.SetStruct(ctx, key, value, expiration)
|
||||
}
|
||||
|
||||
func (m *Manager) Get(ctx context.Context, key string, dest interface{}) error {
|
||||
return m.cache.GetStruct(ctx, key, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) Delete(ctx context.Context, key string) error {
|
||||
return m.cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
func (m *Manager) Exists(ctx context.Context, key string) (bool, error) {
|
||||
return m.cache.Exists(ctx, key)
|
||||
}
|
||||
|
||||
// Health check
|
||||
func (m *Manager) Health(ctx context.Context) error {
|
||||
return m.cache.Health(ctx)
|
||||
}
|
||||
|
||||
// GetRedisClient mendapatkan Redis client jika tersedia
|
||||
func (m *Manager) GetRedisClient() interface{} {
|
||||
if redisProvider, ok := m.cache.(RedisClientProvider); ok {
|
||||
return redisProvider.GetRedisClient()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close the cache connection
|
||||
func (m *Manager) Close() error {
|
||||
return m.cache.Close()
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/noop.go
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NoOpCache is a no-operation cache implementation for development
|
||||
type NoOpCache struct{}
|
||||
|
||||
// NewNoOpCache creates a new no-op cache instance
|
||||
func NewNoOpCache() Cache {
|
||||
return &NoOpCache{}
|
||||
}
|
||||
|
||||
// Get always returns empty string and error
|
||||
func (n *NoOpCache) Get(ctx context.Context, key string) (string, error) {
|
||||
return "", errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// Set always succeeds but does nothing
|
||||
func (n *NoOpCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete always succeeds but does nothing
|
||||
func (n *NoOpCache) Delete(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists always returns false
|
||||
func (n *NoOpCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetStruct always returns error
|
||||
func (n *NoOpCache) GetStruct(ctx context.Context, key string, dest interface{}) error {
|
||||
return errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// SetStruct always succeeds but does nothing
|
||||
func (n *NoOpCache) SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Increment always returns 0 and error
|
||||
func (n *NoOpCache) Increment(ctx context.Context, key string, delta int64) (int64, error) {
|
||||
return 0, errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// Decrement always returns 0 and error
|
||||
func (n *NoOpCache) Decrement(ctx context.Context, key string, delta int64) (int64, error) {
|
||||
return 0, errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// TTL always returns 0 and error
|
||||
func (n *NoOpCache) TTL(ctx context.Context, key string) (time.Duration, error) {
|
||||
return 0, errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// Close always succeeds
|
||||
func (n *NoOpCache) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health always succeeds
|
||||
func (n *NoOpCache) Health(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MGet always returns empty slice and error
|
||||
func (n *NoOpCache) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
|
||||
return []interface{}{}, errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// MSet always succeeds but does nothing
|
||||
func (n *NoOpCache) MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/redis.go
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisCache implements Cache interface using Redis
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
config CacheConfig
|
||||
}
|
||||
|
||||
// NewRedisCache creates a new Redis cache instance
|
||||
func NewRedisCache(config CacheConfig) (Cache, error) {
|
||||
if !config.Enabled {
|
||||
return &NoOpCache{}, nil
|
||||
}
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port),
|
||||
Password: config.Redis.Password,
|
||||
DB: config.Redis.DB,
|
||||
})
|
||||
|
||||
// Test connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
return &RedisCache{
|
||||
client: client,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves a value from cache
|
||||
func (r *RedisCache) Get(ctx context.Context, key string) (string, error) {
|
||||
return r.client.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Set stores a value in cache
|
||||
func (r *RedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
return r.client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
// Delete removes a value from cache
|
||||
func (r *RedisCache) Delete(ctx context.Context, key string) error {
|
||||
return r.client.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in cache
|
||||
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||
result, err := r.client.Exists(ctx, key).Result()
|
||||
return result > 0, err
|
||||
}
|
||||
|
||||
// GetStruct retrieves and unmarshals a struct from cache
|
||||
func (r *RedisCache) GetStruct(ctx context.Context, key string, dest interface{}) error {
|
||||
data, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal([]byte(data), dest)
|
||||
}
|
||||
|
||||
// SetStruct marshals and stores a struct in cache
|
||||
func (r *RedisCache) SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value: %w", err)
|
||||
}
|
||||
return r.client.Set(ctx, key, data, expiration).Err()
|
||||
}
|
||||
|
||||
// Increment increments a numeric value in cache
|
||||
func (r *RedisCache) Increment(ctx context.Context, key string, delta int64) (int64, error) {
|
||||
return r.client.IncrBy(ctx, key, delta).Result()
|
||||
}
|
||||
|
||||
// Decrement decrements a numeric value in cache
|
||||
func (r *RedisCache) Decrement(ctx context.Context, key string, delta int64) (int64, error) {
|
||||
return r.client.DecrBy(ctx, key, delta).Result()
|
||||
}
|
||||
|
||||
// TTL returns the time to live for a key
|
||||
func (r *RedisCache) TTL(ctx context.Context, key string) (time.Duration, error) {
|
||||
return r.client.TTL(ctx, key).Result()
|
||||
}
|
||||
|
||||
// GetRedisClient mendapatkan Redis client
|
||||
func (r *RedisCache) GetRedisClient() interface{} {
|
||||
return r.client
|
||||
}
|
||||
|
||||
// Close closes the Redis connection
|
||||
func (r *RedisCache) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
// Health checks the health of the cache
|
||||
func (r *RedisCache) Health(ctx context.Context) error {
|
||||
return r.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
// MGet retrieves multiple values from cache
|
||||
func (r *RedisCache) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
|
||||
return r.client.MGet(ctx, keys...).Result()
|
||||
}
|
||||
|
||||
// MSet stores multiple values in cache
|
||||
func (r *RedisCache) MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||
pipe := r.client.Pipeline()
|
||||
|
||||
for key, value := range items {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value for key %s: %w", key, err)
|
||||
}
|
||||
pipe.Set(ctx, key, data, expiration)
|
||||
}
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
// internal/infrastructure/container/container.go
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/pkg/logger" // Pastikan import ini benar
|
||||
)
|
||||
|
||||
// Container holds all application dependencies
|
||||
type Container struct {
|
||||
Config *config.Config
|
||||
CacheManager *cache.Manager
|
||||
}
|
||||
|
||||
// NewContainer creates a new container with all dependencies
|
||||
func NewContainer(cfg *config.Config) (*Container, error) {
|
||||
// Create cache factory
|
||||
cacheFactory := cache.NewFactory(cfg.Cache)
|
||||
|
||||
// Create cache manager
|
||||
cacheManager, err := cacheFactory.CreateManager()
|
||||
if err != nil {
|
||||
// PERBAIKAN: Gunakan logger baru yang terstruktur
|
||||
logger.Default().Fatal("Failed to create cache manager", logger.ErrorField(err))
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan logger baru yang terstruktur
|
||||
logger.Default().Info("Cache manager initialized successfully")
|
||||
|
||||
return &Container{
|
||||
Config: cfg,
|
||||
CacheManager: cacheManager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes all container resources
|
||||
func (c *Container) Close() error {
|
||||
if c.CacheManager != nil {
|
||||
if err := c.CacheManager.Close(); err != nil {
|
||||
// PERBAIKAN: Gunakan logger baru, log error sebelum mengembalikan
|
||||
logger.Default().Error("Failed to close cache manager", logger.ErrorField(err))
|
||||
return fmt.Errorf("failed to close cache manager: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+7216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
001,JAWA,true
|
||||
002,SUNDA,true
|
||||
003,BATAK,true
|
||||
004,MADURA,true
|
||||
005,BUGIS,true
|
||||
006,MELAYU,true
|
||||
007,MINANG,true
|
||||
008,BETAWI,true
|
||||
009,BANJAR,true
|
||||
010,BALINESE,true
|
||||
011,ACEH,true
|
||||
012,DAYAK,true
|
||||
013,SASAK,true
|
||||
014,CHINESE,true
|
||||
015,ARAB,true
|
||||
016,INDIA,true
|
||||
017,AMBON,true
|
||||
018,PAPUA,true
|
||||
019,MINAHASA,true
|
||||
020,TORAJA,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
IGD,INSTALASI GAWAT DARURAT,true
|
||||
RI,RAWAT INAP,true
|
||||
RJ,RAWAT JALAN,true
|
||||
OK,INSTALASI KAMAR OPERASI,true
|
||||
VK,INSTALASI KEBIDANAN,true
|
||||
LAB,INSTALASI LABORATORIUM,true
|
||||
RAD,INSTALASI RADIOLOGI,true
|
||||
FAR,INSTALASI FARMASI,true
|
||||
GIZI,INSTALASI GIZI,true
|
||||
REHAB,INSTALASI REHABILITASI,true
|
||||
INTENSIVE,INSTALASI INTENSIVE,true
|
||||
NICU,INSTALASI NICU,true
|
||||
PICU,INSTALASI PICU,true
|
||||
ICU,INSTALASI ICU,true
|
||||
OKULARIS,INSTALASI OKULARIS,true
|
||||
KARDIO,INSTALASI KARDIOVASKULER,true
|
||||
NEURO,INSTALASI NEUROLOGI,true
|
||||
ONKO,INSTALASI ONKOLOGI,true
|
||||
GERIATRI,INSTALASI GERIATRI,true
|
||||
PSIKIATRI,INSTALASI PSIKIATRI,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
ID,INDONESIA,true
|
||||
EN,ENGLISH,true
|
||||
JV,JAWA,true
|
||||
SU,SUNDA,true
|
||||
MA,MADURA,true
|
||||
MI,MINANG,true
|
||||
BT,BATAK,true
|
||||
BG,BUGIS,true
|
||||
ML,MELAYU,true
|
||||
BN,BANJAR,true
|
||||
BL,BALINESE,true
|
||||
AC,ACEH,true
|
||||
DY,DAYAK,true
|
||||
SS,SASAK,true
|
||||
ZH,CHINESE,true
|
||||
AR,ARABIC,true
|
||||
HI,HINDI,true
|
||||
AM,AMBONESE,true
|
||||
PP,PAPUAN,true
|
||||
MH,MINAHASA,true
|
||||
|
+35
@@ -0,0 +1,35 @@
|
||||
Code,Name
|
||||
11,ACEH
|
||||
12,SUMATERA UTARA
|
||||
13,SUMATERA BARAT
|
||||
14,RIAU
|
||||
15,JAMBI
|
||||
16,SUMATERA SELATAN
|
||||
17,BENGKULU
|
||||
18,LAMPUNG
|
||||
19,KEPULAUAN BANGKA BELITUNG
|
||||
21,KEPULAUAN RIAU
|
||||
31,DKI JAKARTA
|
||||
32,JAWA BARAT
|
||||
33,JAWA TENGAH
|
||||
34,DI YOGYAKARTA
|
||||
35,JAWA TIMUR
|
||||
36,BANTEN
|
||||
51,BALI
|
||||
52,NUSA TENGGARA BARAT
|
||||
53,NUSA TENGGARA TIMUR
|
||||
61,KALIMANTAN BARAT
|
||||
62,KALIMANTAN TENGAH
|
||||
63,KALIMANTAN SELATAN
|
||||
64,KALIMANTAN TIMUR
|
||||
65,KALIMANTAN UTARA
|
||||
71,SULAWESI UTARA
|
||||
72,SULAWESI TENGAH
|
||||
73,SULAWESI SELATAN
|
||||
74,SULAWESI TENGGARA
|
||||
75,GORONTALO
|
||||
76,SULAWESI BARAT
|
||||
81,MALUKU
|
||||
82,MALUKU UTARA
|
||||
91,PAPUA BARAT
|
||||
94,PAPUA
|
||||
|
+515
@@ -0,0 +1,515 @@
|
||||
Code,Province_Code,Name
|
||||
1101,11,KABUPATEN SIMEULUE
|
||||
1102,11,KABUPATEN ACEH SINGKIL
|
||||
1103,11,KABUPATEN ACEH SELATAN
|
||||
1104,11,KABUPATEN ACEH TENGGARA
|
||||
1105,11,KABUPATEN ACEH TIMUR
|
||||
1106,11,KABUPATEN ACEH TENGAH
|
||||
1107,11,KABUPATEN ACEH BARAT
|
||||
1108,11,KABUPATEN ACEH BESAR
|
||||
1109,11,KABUPATEN PIDIE
|
||||
1110,11,KABUPATEN BIREUEN
|
||||
1111,11,KABUPATEN ACEH UTARA
|
||||
1112,11,KABUPATEN ACEH BARAT DAYA
|
||||
1113,11,KABUPATEN GAYO LUES
|
||||
1114,11,KABUPATEN ACEH TAMIANG
|
||||
1115,11,KABUPATEN NAGAN RAYA
|
||||
1116,11,KABUPATEN ACEH JAYA
|
||||
1117,11,KABUPATEN BENER MERIAH
|
||||
1118,11,KABUPATEN PIDIE JAYA
|
||||
1171,11,KOTA BANDA ACEH
|
||||
1172,11,KOTA SABANG
|
||||
1173,11,KOTA LANGSA
|
||||
1174,11,KOTA LHOKSEUMAWE
|
||||
1175,11,KOTA SUBULUSSALAM
|
||||
1201,12,KABUPATEN NIAS
|
||||
1202,12,KABUPATEN MANDAILING NATAL
|
||||
1203,12,KABUPATEN TAPANULI SELATAN
|
||||
1204,12,KABUPATEN TAPANULI TENGAH
|
||||
1205,12,KABUPATEN TAPANULI UTARA
|
||||
1206,12,KABUPATEN TOBA SAMOSIR
|
||||
1207,12,KABUPATEN LABUHAN BATU
|
||||
1208,12,KABUPATEN ASAHAN
|
||||
1209,12,KABUPATEN SIMALUNGUN
|
||||
1210,12,KABUPATEN DAIRI
|
||||
1211,12,KABUPATEN KARO
|
||||
1212,12,KABUPATEN DELI SERDANG
|
||||
1213,12,KABUPATEN LANGKAT
|
||||
1214,12,KABUPATEN NIAS SELATAN
|
||||
1215,12,KABUPATEN HUMBANG HASUNDUTAN
|
||||
1216,12,KABUPATEN PAKPAK BHARAT
|
||||
1217,12,KABUPATEN SAMOSIR
|
||||
1218,12,KABUPATEN SERDANG BEDAGAI
|
||||
1219,12,KABUPATEN BATU BARA
|
||||
1220,12,KABUPATEN PADANG LAWAS UTARA
|
||||
1221,12,KABUPATEN PADANG LAWAS
|
||||
1222,12,KABUPATEN LABUHAN BATU SELATAN
|
||||
1223,12,KABUPATEN LABUHAN BATU UTARA
|
||||
1224,12,KABUPATEN NIAS UTARA
|
||||
1225,12,KABUPATEN NIAS BARAT
|
||||
1271,12,KOTA SIBOLGA
|
||||
1272,12,KOTA TANJUNG BALAI
|
||||
1273,12,KOTA PEMATANG SIANTAR
|
||||
1274,12,KOTA TEBING TINGGI
|
||||
1275,12,KOTA MEDAN
|
||||
1276,12,KOTA BINJAI
|
||||
1277,12,KOTA PADANGSIDIMPUAN
|
||||
1278,12,KOTA GUNUNGSITOLI
|
||||
1301,13,KABUPATEN KEPULAUAN MENTAWAI
|
||||
1302,13,KABUPATEN PESISIR SELATAN
|
||||
1303,13,KABUPATEN SOLOK
|
||||
1304,13,KABUPATEN SIJUNJUNG
|
||||
1305,13,KABUPATEN TANAH DATAR
|
||||
1306,13,KABUPATEN PADANG PARIAMAN
|
||||
1307,13,KABUPATEN AGAM
|
||||
1308,13,KABUPATEN LIMA PULUH KOTA
|
||||
1309,13,KABUPATEN PASAMAN
|
||||
1310,13,KABUPATEN SOLOK SELATAN
|
||||
1311,13,KABUPATEN DHARMASRAYA
|
||||
1312,13,KABUPATEN PASAMAN BARAT
|
||||
1371,13,KOTA PADANG
|
||||
1372,13,KOTA SOLOK
|
||||
1373,13,KOTA SAWAH LUNTO
|
||||
1374,13,KOTA PADANG PANJANG
|
||||
1375,13,KOTA BUKITTINGGI
|
||||
1376,13,KOTA PAYAKUMBUH
|
||||
1377,13,KOTA PARIAMAN
|
||||
1401,14,KABUPATEN KUANTAN SINGINGI
|
||||
1402,14,KABUPATEN INDRAGIRI HULU
|
||||
1403,14,KABUPATEN INDRAGIRI HILIR
|
||||
1404,14,KABUPATEN PELALAWAN
|
||||
1405,14,KABUPATEN S I A K
|
||||
1406,14,KABUPATEN KAMPAR
|
||||
1407,14,KABUPATEN ROKAN HULU
|
||||
1408,14,KABUPATEN BENGKALIS
|
||||
1409,14,KABUPATEN ROKAN HILIR
|
||||
1410,14,KABUPATEN KEPULAUAN MERANTI
|
||||
1471,14,KOTA PEKANBARU
|
||||
1473,14,KOTA D U M A I
|
||||
1501,15,KABUPATEN KERINCI
|
||||
1502,15,KABUPATEN MERANGIN
|
||||
1503,15,KABUPATEN SAROLANGUN
|
||||
1504,15,KABUPATEN BATANG HARI
|
||||
1505,15,KABUPATEN MUARO JAMBI
|
||||
1506,15,KABUPATEN TANJUNG JABUNG TIMUR
|
||||
1507,15,KABUPATEN TANJUNG JABUNG BARAT
|
||||
1508,15,KABUPATEN TEBO
|
||||
1509,15,KABUPATEN BUNGO
|
||||
1571,15,KOTA JAMBI
|
||||
1572,15,KOTA SUNGAI PENUH
|
||||
1601,16,KABUPATEN OGAN KOMERING ULU
|
||||
1602,16,KABUPATEN OGAN KOMERING ILIR
|
||||
1603,16,KABUPATEN MUARA ENIM
|
||||
1604,16,KABUPATEN LAHAT
|
||||
1605,16,KABUPATEN MUSI RAWAS
|
||||
1606,16,KABUPATEN MUSI BANYUASIN
|
||||
1607,16,KABUPATEN BANYU ASIN
|
||||
1608,16,KABUPATEN OGAN KOMERING ULU SELATAN
|
||||
1609,16,KABUPATEN OGAN KOMERING ULU TIMUR
|
||||
1610,16,KABUPATEN OGAN ILIR
|
||||
1611,16,KABUPATEN EMPAT LAWANG
|
||||
1612,16,KABUPATEN PENUKAL ABAB LEMATANG ILIR
|
||||
1613,16,KABUPATEN MUSI RAWAS UTARA
|
||||
1671,16,KOTA PALEMBANG
|
||||
1672,16,KOTA PRABUMULIH
|
||||
1673,16,KOTA PAGAR ALAM
|
||||
1674,16,KOTA LUBUKLINGGAU
|
||||
1701,17,KABUPATEN BENGKULU SELATAN
|
||||
1702,17,KABUPATEN REJANG LEBONG
|
||||
1703,17,KABUPATEN BENGKULU UTARA
|
||||
1704,17,KABUPATEN KAUR
|
||||
1705,17,KABUPATEN SELUMA
|
||||
1706,17,KABUPATEN MUKOMUKO
|
||||
1707,17,KABUPATEN LEBONG
|
||||
1708,17,KABUPATEN KEPAHIANG
|
||||
1709,17,KABUPATEN BENGKULU TENGAH
|
||||
1771,17,KOTA BENGKULU
|
||||
1801,18,KABUPATEN LAMPUNG BARAT
|
||||
1802,18,KABUPATEN TANGGAMUS
|
||||
1803,18,KABUPATEN LAMPUNG SELATAN
|
||||
1804,18,KABUPATEN LAMPUNG TIMUR
|
||||
1805,18,KABUPATEN LAMPUNG TENGAH
|
||||
1806,18,KABUPATEN LAMPUNG UTARA
|
||||
1807,18,KABUPATEN WAY KANAN
|
||||
1808,18,KABUPATEN TULANGBAWANG
|
||||
1809,18,KABUPATEN PESAWARAN
|
||||
1810,18,KABUPATEN PRINGSEWU
|
||||
1811,18,KABUPATEN MESUJI
|
||||
1812,18,KABUPATEN TULANG BAWANG BARAT
|
||||
1813,18,KABUPATEN PESISIR BARAT
|
||||
1871,18,KOTA BANDAR LAMPUNG
|
||||
1872,18,KOTA METRO
|
||||
1901,19,KABUPATEN BANGKA
|
||||
1902,19,KABUPATEN BELITUNG
|
||||
1903,19,KABUPATEN BANGKA BARAT
|
||||
1904,19,KABUPATEN BANGKA TENGAH
|
||||
1905,19,KABUPATEN BANGKA SELATAN
|
||||
1906,19,KABUPATEN BELITUNG TIMUR
|
||||
1971,19,KOTA PANGKAL PINANG
|
||||
2101,21,KABUPATEN KARIMUN
|
||||
2102,21,KABUPATEN BINTAN
|
||||
2103,21,KABUPATEN NATUNA
|
||||
2104,21,KABUPATEN LINGGA
|
||||
2105,21,KABUPATEN KEPULAUAN ANAMBAS
|
||||
2171,21,KOTA BATAM
|
||||
2172,21,KOTA TANJUNG PINANG
|
||||
3101,31,KABUPATEN KEPULAUAN SERIBU
|
||||
3171,31,KOTA JAKARTA SELATAN
|
||||
3172,31,KOTA JAKARTA TIMUR
|
||||
3173,31,KOTA JAKARTA PUSAT
|
||||
3174,31,KOTA JAKARTA BARAT
|
||||
3175,31,KOTA JAKARTA UTARA
|
||||
3201,32,KABUPATEN BOGOR
|
||||
3202,32,KABUPATEN SUKABUMI
|
||||
3203,32,KABUPATEN CIANJUR
|
||||
3204,32,KABUPATEN BANDUNG
|
||||
3205,32,KABUPATEN GARUT
|
||||
3206,32,KABUPATEN TASIKMALAYA
|
||||
3207,32,KABUPATEN CIAMIS
|
||||
3208,32,KABUPATEN KUNINGAN
|
||||
3209,32,KABUPATEN CIREBON
|
||||
3210,32,KABUPATEN MAJALENGKA
|
||||
3211,32,KABUPATEN SUMEDANG
|
||||
3212,32,KABUPATEN INDRAMAYU
|
||||
3213,32,KABUPATEN SUBANG
|
||||
3214,32,KABUPATEN PURWAKARTA
|
||||
3215,32,KABUPATEN KARAWANG
|
||||
3216,32,KABUPATEN BEKASI
|
||||
3217,32,KABUPATEN BANDUNG BARAT
|
||||
3218,32,KABUPATEN PANGANDARAN
|
||||
3271,32,KOTA BOGOR
|
||||
3272,32,KOTA SUKABUMI
|
||||
3273,32,KOTA BANDUNG
|
||||
3274,32,KOTA CIREBON
|
||||
3275,32,KOTA BEKASI
|
||||
3276,32,KOTA DEPOK
|
||||
3277,32,KOTA CIMAHI
|
||||
3278,32,KOTA TASIKMALAYA
|
||||
3279,32,KOTA BANJAR
|
||||
3301,33,KABUPATEN CILACAP
|
||||
3302,33,KABUPATEN BANYUMAS
|
||||
3303,33,KABUPATEN PURBALINGGA
|
||||
3304,33,KABUPATEN BANJARNEGARA
|
||||
3305,33,KABUPATEN KEBUMEN
|
||||
3306,33,KABUPATEN PURWOREJO
|
||||
3307,33,KABUPATEN WONOSOBO
|
||||
3308,33,KABUPATEN MAGELANG
|
||||
3309,33,KABUPATEN BOYOLALI
|
||||
3310,33,KABUPATEN KLATEN
|
||||
3311,33,KABUPATEN SUKOHARJO
|
||||
3312,33,KABUPATEN WONOGIRI
|
||||
3313,33,KABUPATEN KARANGANYAR
|
||||
3314,33,KABUPATEN SRAGEN
|
||||
3315,33,KABUPATEN GROBOGAN
|
||||
3316,33,KABUPATEN BLORA
|
||||
3317,33,KABUPATEN REMBANG
|
||||
3318,33,KABUPATEN PATI
|
||||
3319,33,KABUPATEN KUDUS
|
||||
3320,33,KABUPATEN JEPARA
|
||||
3321,33,KABUPATEN DEMAK
|
||||
3322,33,KABUPATEN SEMARANG
|
||||
3323,33,KABUPATEN TEMANGGUNG
|
||||
3324,33,KABUPATEN KENDAL
|
||||
3325,33,KABUPATEN BATANG
|
||||
3326,33,KABUPATEN PEKALONGAN
|
||||
3327,33,KABUPATEN PEMALANG
|
||||
3328,33,KABUPATEN TEGAL
|
||||
3329,33,KABUPATEN BREBES
|
||||
3371,33,KOTA MAGELANG
|
||||
3372,33,KOTA SURAKARTA
|
||||
3373,33,KOTA SALATIGA
|
||||
3374,33,KOTA SEMARANG
|
||||
3375,33,KOTA PEKALONGAN
|
||||
3376,33,KOTA TEGAL
|
||||
3401,34,KABUPATEN KULON PROGO
|
||||
3402,34,KABUPATEN BANTUL
|
||||
3403,34,KABUPATEN GUNUNG KIDUL
|
||||
3404,34,KABUPATEN SLEMAN
|
||||
3471,34,KOTA YOGYAKARTA
|
||||
3501,35,KABUPATEN PACITAN
|
||||
3502,35,KABUPATEN PONOROGO
|
||||
3503,35,KABUPATEN TRENGGALEK
|
||||
3504,35,KABUPATEN TULUNGAGUNG
|
||||
3505,35,KABUPATEN BLITAR
|
||||
3506,35,KABUPATEN KEDIRI
|
||||
3507,35,KABUPATEN MALANG
|
||||
3508,35,KABUPATEN LUMAJANG
|
||||
3509,35,KABUPATEN JEMBER
|
||||
3510,35,KABUPATEN BANYUWANGI
|
||||
3511,35,KABUPATEN BONDOWOSO
|
||||
3512,35,KABUPATEN SITUBONDO
|
||||
3513,35,KABUPATEN PROBOLINGGO
|
||||
3514,35,KABUPATEN PASURUAN
|
||||
3515,35,KABUPATEN SIDOARJO
|
||||
3516,35,KABUPATEN MOJOKERTO
|
||||
3517,35,KABUPATEN JOMBANG
|
||||
3518,35,KABUPATEN NGANJUK
|
||||
3519,35,KABUPATEN MADIUN
|
||||
3520,35,KABUPATEN MAGETAN
|
||||
3521,35,KABUPATEN NGAWI
|
||||
3522,35,KABUPATEN BOJONEGORO
|
||||
3523,35,KABUPATEN TUBAN
|
||||
3524,35,KABUPATEN LAMONGAN
|
||||
3525,35,KABUPATEN GRESIK
|
||||
3526,35,KABUPATEN BANGKALAN
|
||||
3527,35,KABUPATEN SAMPANG
|
||||
3528,35,KABUPATEN PAMEKASAN
|
||||
3529,35,KABUPATEN SUMENEP
|
||||
3571,35,KOTA KEDIRI
|
||||
3572,35,KOTA BLITAR
|
||||
3573,35,KOTA MALANG
|
||||
3574,35,KOTA PROBOLINGGO
|
||||
3575,35,KOTA PASURUAN
|
||||
3576,35,KOTA MOJOKERTO
|
||||
3577,35,KOTA MADIUN
|
||||
3578,35,KOTA SURABAYA
|
||||
3579,35,KOTA BATU
|
||||
3601,36,KABUPATEN PANDEGLANG
|
||||
3602,36,KABUPATEN LEBAK
|
||||
3603,36,KABUPATEN TANGERANG
|
||||
3604,36,KABUPATEN SERANG
|
||||
3671,36,KOTA TANGERANG
|
||||
3672,36,KOTA CILEGON
|
||||
3673,36,KOTA SERANG
|
||||
3674,36,KOTA TANGERANG SELATAN
|
||||
5101,51,KABUPATEN JEMBRANA
|
||||
5102,51,KABUPATEN TABANAN
|
||||
5103,51,KABUPATEN BADUNG
|
||||
5104,51,KABUPATEN GIANYAR
|
||||
5105,51,KABUPATEN KLUNGKUNG
|
||||
5106,51,KABUPATEN BANGLI
|
||||
5107,51,KABUPATEN KARANG ASEM
|
||||
5108,51,KABUPATEN BULELENG
|
||||
5171,51,KOTA DENPASAR
|
||||
5201,52,KABUPATEN LOMBOK BARAT
|
||||
5202,52,KABUPATEN LOMBOK TENGAH
|
||||
5203,52,KABUPATEN LOMBOK TIMUR
|
||||
5204,52,KABUPATEN SUMBAWA
|
||||
5205,52,KABUPATEN DOMPU
|
||||
5206,52,KABUPATEN BIMA
|
||||
5207,52,KABUPATEN SUMBAWA BARAT
|
||||
5208,52,KABUPATEN LOMBOK UTARA
|
||||
5271,52,KOTA MATARAM
|
||||
5272,52,KOTA BIMA
|
||||
5301,53,KABUPATEN SUMBA BARAT
|
||||
5302,53,KABUPATEN SUMBA TIMUR
|
||||
5303,53,KABUPATEN KUPANG
|
||||
5304,53,KABUPATEN TIMOR TENGAH SELATAN
|
||||
5305,53,KABUPATEN TIMOR TENGAH UTARA
|
||||
5306,53,KABUPATEN BELU
|
||||
5307,53,KABUPATEN ALOR
|
||||
5308,53,KABUPATEN LEMBATA
|
||||
5309,53,KABUPATEN FLORES TIMUR
|
||||
5310,53,KABUPATEN SIKKA
|
||||
5311,53,KABUPATEN ENDE
|
||||
5312,53,KABUPATEN NGADA
|
||||
5313,53,KABUPATEN MANGGARAI
|
||||
5314,53,KABUPATEN ROTE NDAO
|
||||
5315,53,KABUPATEN MANGGARAI BARAT
|
||||
5316,53,KABUPATEN SUMBA TENGAH
|
||||
5317,53,KABUPATEN SUMBA BARAT DAYA
|
||||
5318,53,KABUPATEN NAGEKEO
|
||||
5319,53,KABUPATEN MANGGARAI TIMUR
|
||||
5320,53,KABUPATEN SABU RAIJUA
|
||||
5321,53,KABUPATEN MALAKA
|
||||
5371,53,KOTA KUPANG
|
||||
6101,61,KABUPATEN SAMBAS
|
||||
6102,61,KABUPATEN BENGKAYANG
|
||||
6103,61,KABUPATEN LANDAK
|
||||
6104,61,KABUPATEN MEMPAWAH
|
||||
6105,61,KABUPATEN SANGGAU
|
||||
6106,61,KABUPATEN KETAPANG
|
||||
6107,61,KABUPATEN SINTANG
|
||||
6108,61,KABUPATEN KAPUAS HULU
|
||||
6109,61,KABUPATEN SEKADAU
|
||||
6110,61,KABUPATEN MELAWI
|
||||
6111,61,KABUPATEN KAYONG UTARA
|
||||
6112,61,KABUPATEN KUBU RAYA
|
||||
6171,61,KOTA PONTIANAK
|
||||
6172,61,KOTA SINGKAWANG
|
||||
6201,62,KABUPATEN KOTAWARINGIN BARAT
|
||||
6202,62,KABUPATEN KOTAWARINGIN TIMUR
|
||||
6203,62,KABUPATEN KAPUAS
|
||||
6204,62,KABUPATEN BARITO SELATAN
|
||||
6205,62,KABUPATEN BARITO UTARA
|
||||
6206,62,KABUPATEN SUKAMARA
|
||||
6207,62,KABUPATEN LAMANDAU
|
||||
6208,62,KABUPATEN SERUYAN
|
||||
6209,62,KABUPATEN KATINGAN
|
||||
6210,62,KABUPATEN PULANG PISAU
|
||||
6211,62,KABUPATEN GUNUNG MAS
|
||||
6212,62,KABUPATEN BARITO TIMUR
|
||||
6213,62,KABUPATEN MURUNG RAYA
|
||||
6271,62,KOTA PALANGKA RAYA
|
||||
6301,63,KABUPATEN TANAH LAUT
|
||||
6302,63,KABUPATEN KOTA BARU
|
||||
6303,63,KABUPATEN BANJAR
|
||||
6304,63,KABUPATEN BARITO KUALA
|
||||
6305,63,KABUPATEN TAPIN
|
||||
6306,63,KABUPATEN HULU SUNGAI SELATAN
|
||||
6307,63,KABUPATEN HULU SUNGAI TENGAH
|
||||
6308,63,KABUPATEN HULU SUNGAI UTARA
|
||||
6309,63,KABUPATEN TABALONG
|
||||
6310,63,KABUPATEN TANAH BUMBU
|
||||
6311,63,KABUPATEN BALANGAN
|
||||
6371,63,KOTA BANJARMASIN
|
||||
6372,63,KOTA BANJAR BARU
|
||||
6401,64,KABUPATEN PASER
|
||||
6402,64,KABUPATEN KUTAI BARAT
|
||||
6403,64,KABUPATEN KUTAI KARTANEGARA
|
||||
6404,64,KABUPATEN KUTAI TIMUR
|
||||
6405,64,KABUPATEN BERAU
|
||||
6409,64,KABUPATEN PENAJAM PASER UTARA
|
||||
6411,64,KABUPATEN MAHAKAM HULU
|
||||
6471,64,KOTA BALIKPAPAN
|
||||
6472,64,KOTA SAMARINDA
|
||||
6474,64,KOTA BONTANG
|
||||
6501,65,KABUPATEN MALINAU
|
||||
6502,65,KABUPATEN BULUNGAN
|
||||
6503,65,KABUPATEN TANA TIDUNG
|
||||
6504,65,KABUPATEN NUNUKAN
|
||||
6571,65,KOTA TARAKAN
|
||||
7101,71,KABUPATEN BOLAANG MONGONDOW
|
||||
7102,71,KABUPATEN MINAHASA
|
||||
7103,71,KABUPATEN KEPULAUAN SANGIHE
|
||||
7104,71,KABUPATEN KEPULAUAN TALAUD
|
||||
7105,71,KABUPATEN MINAHASA SELATAN
|
||||
7106,71,KABUPATEN MINAHASA UTARA
|
||||
7107,71,KABUPATEN BOLAANG MONGONDOW UTARA
|
||||
7108,71,KABUPATEN SIAU TAGULANDANG BIARO
|
||||
7109,71,KABUPATEN MINAHASA TENGGARA
|
||||
7110,71,KABUPATEN BOLAANG MONGONDOW SELATAN
|
||||
7111,71,KABUPATEN BOLAANG MONGONDOW TIMUR
|
||||
7171,71,KOTA MANADO
|
||||
7172,71,KOTA BITUNG
|
||||
7173,71,KOTA TOMOHON
|
||||
7174,71,KOTA KOTAMOBAGU
|
||||
7201,72,KABUPATEN BANGGAI KEPULAUAN
|
||||
7202,72,KABUPATEN BANGGAI
|
||||
7203,72,KABUPATEN MOROWALI
|
||||
7204,72,KABUPATEN POSO
|
||||
7205,72,KABUPATEN DONGGALA
|
||||
7206,72,KABUPATEN TOLI-TOLI
|
||||
7207,72,KABUPATEN BUOL
|
||||
7208,72,KABUPATEN PARIGI MOUTONG
|
||||
7209,72,KABUPATEN TOJO UNA-UNA
|
||||
7210,72,KABUPATEN SIGI
|
||||
7211,72,KABUPATEN BANGGAI LAUT
|
||||
7212,72,KABUPATEN MOROWALI UTARA
|
||||
7271,72,KOTA PALU
|
||||
7301,73,KABUPATEN KEPULAUAN SELAYAR
|
||||
7302,73,KABUPATEN BULUKUMBA
|
||||
7303,73,KABUPATEN BANTAENG
|
||||
7304,73,KABUPATEN JENEPONTO
|
||||
7305,73,KABUPATEN TAKALAR
|
||||
7306,73,KABUPATEN GOWA
|
||||
7307,73,KABUPATEN SINJAI
|
||||
7308,73,KABUPATEN MAROS
|
||||
7309,73,KABUPATEN PANGKAJENE DAN KEPULAUAN
|
||||
7310,73,KABUPATEN BARRU
|
||||
7311,73,KABUPATEN BONE
|
||||
7312,73,KABUPATEN SOPPENG
|
||||
7313,73,KABUPATEN WAJO
|
||||
7314,73,KABUPATEN SIDENRENG RAPPANG
|
||||
7315,73,KABUPATEN PINRANG
|
||||
7316,73,KABUPATEN ENREKANG
|
||||
7317,73,KABUPATEN LUWU
|
||||
7318,73,KABUPATEN TANA TORAJA
|
||||
7322,73,KABUPATEN LUWU UTARA
|
||||
7325,73,KABUPATEN LUWU TIMUR
|
||||
7326,73,KABUPATEN TORAJA UTARA
|
||||
7371,73,KOTA MAKASSAR
|
||||
7372,73,KOTA PAREPARE
|
||||
7373,73,KOTA PALOPO
|
||||
7401,74,KABUPATEN BUTON
|
||||
7402,74,KABUPATEN MUNA
|
||||
7403,74,KABUPATEN KONAWE
|
||||
7404,74,KABUPATEN KOLAKA
|
||||
7405,74,KABUPATEN KONAWE SELATAN
|
||||
7406,74,KABUPATEN BOMBANA
|
||||
7407,74,KABUPATEN WAKATOBI
|
||||
7408,74,KABUPATEN KOLAKA UTARA
|
||||
7409,74,KABUPATEN BUTON UTARA
|
||||
7410,74,KABUPATEN KONAWE UTARA
|
||||
7411,74,KABUPATEN KOLAKA TIMUR
|
||||
7412,74,KABUPATEN KONAWE KEPULAUAN
|
||||
7413,74,KABUPATEN MUNA BARAT
|
||||
7414,74,KABUPATEN BUTON TENGAH
|
||||
7415,74,KABUPATEN BUTON SELATAN
|
||||
7471,74,KOTA KENDARI
|
||||
7472,74,KOTA BAUBAU
|
||||
7501,75,KABUPATEN BOALEMO
|
||||
7502,75,KABUPATEN GORONTALO
|
||||
7503,75,KABUPATEN POHUWATO
|
||||
7504,75,KABUPATEN BONE BOLANGO
|
||||
7505,75,KABUPATEN GORONTALO UTARA
|
||||
7571,75,KOTA GORONTALO
|
||||
7601,76,KABUPATEN MAJENE
|
||||
7602,76,KABUPATEN POLEWALI MANDAR
|
||||
7603,76,KABUPATEN MAMASA
|
||||
7604,76,KABUPATEN MAMUJU
|
||||
7605,76,KABUPATEN MAMUJU UTARA
|
||||
7606,76,KABUPATEN MAMUJU TENGAH
|
||||
8101,81,KABUPATEN MALUKU TENGGARA BARAT
|
||||
8102,81,KABUPATEN MALUKU TENGGARA
|
||||
8103,81,KABUPATEN MALUKU TENGAH
|
||||
8104,81,KABUPATEN BURU
|
||||
8105,81,KABUPATEN KEPULAUAN ARU
|
||||
8106,81,KABUPATEN SERAM BAGIAN BARAT
|
||||
8107,81,KABUPATEN SERAM BAGIAN TIMUR
|
||||
8108,81,KABUPATEN MALUKU BARAT DAYA
|
||||
8109,81,KABUPATEN BURU SELATAN
|
||||
8171,81,KOTA AMBON
|
||||
8172,81,KOTA TUAL
|
||||
8201,82,KABUPATEN HALMAHERA BARAT
|
||||
8202,82,KABUPATEN HALMAHERA TENGAH
|
||||
8203,82,KABUPATEN KEPULAUAN SULA
|
||||
8204,82,KABUPATEN HALMAHERA SELATAN
|
||||
8205,82,KABUPATEN HALMAHERA UTARA
|
||||
8206,82,KABUPATEN HALMAHERA TIMUR
|
||||
8207,82,KABUPATEN PULAU MOROTAI
|
||||
8208,82,KABUPATEN PULAU TALIABU
|
||||
8271,82,KOTA TERNATE
|
||||
8272,82,KOTA TIDORE KEPULAUAN
|
||||
9101,91,KABUPATEN FAKFAK
|
||||
9102,91,KABUPATEN KAIMANA
|
||||
9103,91,KABUPATEN TELUK WONDAMA
|
||||
9104,91,KABUPATEN TELUK BINTUNI
|
||||
9105,91,KABUPATEN MANOKWARI
|
||||
9106,91,KABUPATEN SORONG SELATAN
|
||||
9107,91,KABUPATEN SORONG
|
||||
9108,91,KABUPATEN RAJA AMPAT
|
||||
9109,91,KABUPATEN TAMBRAUW
|
||||
9110,91,KABUPATEN MAYBRAT
|
||||
9111,91,KABUPATEN MANOKWARI SELATAN
|
||||
9112,91,KABUPATEN PEGUNUNGAN ARFAK
|
||||
9171,91,KOTA SORONG
|
||||
9401,94,KABUPATEN MERAUKE
|
||||
9402,94,KABUPATEN JAYAWIJAYA
|
||||
9403,94,KABUPATEN JAYAPURA
|
||||
9404,94,KABUPATEN NABIRE
|
||||
9408,94,KABUPATEN KEPULAUAN YAPEN
|
||||
9409,94,KABUPATEN BIAK NUMFOR
|
||||
9410,94,KABUPATEN PANIAI
|
||||
9411,94,KABUPATEN PUNCAK JAYA
|
||||
9412,94,KABUPATEN MIMIKA
|
||||
9413,94,KABUPATEN BOVEN DIGOEL
|
||||
9414,94,KABUPATEN MAPPI
|
||||
9415,94,KABUPATEN ASMAT
|
||||
9416,94,KABUPATEN YAHUKIMO
|
||||
9417,94,KABUPATEN PEGUNUNGAN BINTANG
|
||||
9418,94,KABUPATEN TOLIKARA
|
||||
9419,94,KABUPATEN SARMI
|
||||
9420,94,KABUPATEN KEEROM
|
||||
9426,94,KABUPATEN WAROPEN
|
||||
9427,94,KABUPATEN SUPIORI
|
||||
9428,94,KABUPATEN MAMBERAMO RAYA
|
||||
9429,94,KABUPATEN NDUGA
|
||||
9430,94,KABUPATEN LANNY JAYA
|
||||
9431,94,KABUPATEN MAMBERAMO TENGAH
|
||||
9432,94,KABUPATEN YALIMO
|
||||
9433,94,KABUPATEN PUNCAK
|
||||
9434,94,KABUPATEN DOGIYAI
|
||||
9435,94,KABUPATEN INTAN JAYA
|
||||
9436,94,KABUPATEN DEIYAI
|
||||
9471,94,KOTA JAYAPURA
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
SP001,DOKTER UMUM,true
|
||||
SP002,DOKTER ANAK,true
|
||||
SP003,DOKTER KANDUNGAN,true
|
||||
SP004,DOKTER BEDAH,true
|
||||
SP005,DOKTER JANTUNG,true
|
||||
SP006,DOKTER SARAF,true
|
||||
SP007,DOKTER THT,true
|
||||
SP008,DOKTER MATA,true
|
||||
SP009,DOKTER KULIT DAN KELAMIN,true
|
||||
SP010,DOKTER GIGI,true
|
||||
SP011,DOKTER PSIKIATRI,true
|
||||
SP012,DOKTER ORTHOPEDI,true
|
||||
SP013,DOKTER UROLOGI,true
|
||||
SP014,DOKTER PARU,true
|
||||
SP015,DOKTER GIZI,true
|
||||
SP016,DOKTER REHABILITASI MEDIS,true
|
||||
SP017,DOKTER GERIATRI,true
|
||||
SP018,DOKTER ONKOLOGI,true
|
||||
SP019,DOKTER KARDIOVASKULER,true
|
||||
SP020,DOKTER NEFROLOGI,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Specialist_Code,Name,Status
|
||||
SS001,SP001,MEDICINE,true
|
||||
SS002,SP002,PEDIATRICS,true
|
||||
SS003,SP003,OBSTETRICS,true
|
||||
SS004,SP004,SURGERY,true
|
||||
SS005,SP005,CARDIOLOGY,true
|
||||
SS006,SP006,NEUROLOGY,true
|
||||
SS007,SP007,OTORHINOLARYNGOLOGY,true
|
||||
SS008,SP008,OPHTHALMOLOGY,true
|
||||
SS009,SP009,DERMATOLOGY,true
|
||||
SS010,SP010,DENTISTRY,true
|
||||
SS011,SP011,PSYCHIATRY,true
|
||||
SS012,SP012,ORTHOPEDICS,true
|
||||
SS013,SP013,UROLOGY,true
|
||||
SS014,SP014,PULMONOLOGY,true
|
||||
SS015,SP015,NUTRITION,true
|
||||
SS016,SP016,PHYSICAL MEDICINE,true
|
||||
SS017,SP017,GERIATRICS,true
|
||||
SS018,SP018,ONCOLOGY,true
|
||||
SS019,SP019,CARDIOVASCULAR,true
|
||||
SS020,SP020,NEPHROLOGY,true
|
||||
|
@@ -0,0 +1,21 @@
|
||||
Code,Name,Status
|
||||
001,POLI UMUM,true
|
||||
002,POLI ANAK,true
|
||||
003,POLI KANDUNGAN,true
|
||||
004,POLI BEDAH,true
|
||||
005,POLI JANTUNG,true
|
||||
006,POLI SARAF,true
|
||||
007,POLI THT,true
|
||||
008,POLI MATA,true
|
||||
009,POLI KULIT,true
|
||||
010,POLI GIGI,true
|
||||
011,POLI PSIKIATRI,true
|
||||
012,POLI ORTHOPEDI,true
|
||||
013,POLI UROLOGI,true
|
||||
014,POLI PARU,true
|
||||
015,POLI GIZI,true
|
||||
016,POLI REHABILITASI,true
|
||||
017,POLI GERIATRI,true
|
||||
018,POLI ONKOLOGI,true
|
||||
019,POLI KARDIOVASKULER,true
|
||||
020,POLI NEFROLOGI,true
|
||||
|
+80535
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
// internal/infrastructure/database/database.go
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"service-general/internal/infrastructure/config"
|
||||
"service-general/pkg/logger" // Import logger Anda
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq" // PASTIKAN IMPORT INI ADA
|
||||
|
||||
// Driver GORM di-import secara eksplisit untuk Migration
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger" // Import logger dari GORM
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ... (tipe DatabaseType dan interface Service tetap sama)
|
||||
|
||||
type service struct {
|
||||
// GORM DB adalah sumber utama
|
||||
gormDatabases map[string]*gorm.DB
|
||||
// Map untuk menyimpan koneksi mentah yang diekstrak dari GORM
|
||||
sqlDatabases map[string]*sql.DB
|
||||
sqlxDatabases map[string]*sqlx.DB
|
||||
mongoClients map[string]*mongo.Client
|
||||
readReplicas map[string][]*sql.DB
|
||||
configs map[string]config.DatabaseConfig
|
||||
readConfigs map[string][]config.DatabaseConfig
|
||||
mu sync.RWMutex
|
||||
readBalancer map[string]int
|
||||
// PERBAIKAN 1: Kembali ke tipe yang benar untuk listener PostgreSQL
|
||||
listeners map[string]*pq.Listener
|
||||
listenersMu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
dbManager *service
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// ... (fungsi New dan loadFromConfig tetap sama)
|
||||
|
||||
// --- PERBAIKAN 2: Buat Adapter untuk Logger GORM ---
|
||||
|
||||
// gormLoggerAdapter adalah adapter untuk menghubungkan logger kita dengan GORM
|
||||
type gormLoggerAdapter struct {
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewGormLoggerAdapter membuat instance baru dari adapter
|
||||
func NewGormLoggerAdapter(l logger.Logger) logger.Interface {
|
||||
// Tentukan level log GORM berdasarkan level logger kita
|
||||
// Ini adalah implementasi sederhana, Anda bisa membuatnya lebih dinamis
|
||||
logLevel := logger.Silent
|
||||
if l != nil {
|
||||
// Anda bisa menambahkan logika untuk menentukan level dari logger Anda
|
||||
// Untuk sekarang, kita hardcode ke Info
|
||||
logLevel = logger.Info
|
||||
}
|
||||
|
||||
return &gormLoggerAdapter{
|
||||
logger: l,
|
||||
// Konfigurasi logger GORM
|
||||
Config: logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logLevel,
|
||||
IgnoreRecordNotFoundError: false,
|
||||
Colorful: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LogMode menetapkan level log
|
||||
func (l *gormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newLogger := *l
|
||||
newLogger.Config.LogLevel = level
|
||||
return &newLogger
|
||||
}
|
||||
|
||||
// Info mencatat log info
|
||||
func (l *gormLoggerAdapter) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Info(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Warn mencatat log warning
|
||||
func (l *gormLoggerAdapter) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Warn(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Error mencatat log error
|
||||
func (l *gormLoggerAdapter) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.logger.WithContext(ctx).Error(fmt.Sprintf(msg, data...))
|
||||
}
|
||||
|
||||
// Trace mencatat log trace (SQL query)
|
||||
func (l *gormLoggerAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
sql, rows := fc()
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"duration": elapsed,
|
||||
"rows": rows,
|
||||
"sql": sql,
|
||||
"error": err,
|
||||
"function": "database.trace",
|
||||
}
|
||||
|
||||
switch {
|
||||
case err != nil && l.Config.LogLevel >= logger.Error:
|
||||
l.logger.WithContext(ctx).WithFields(logger.Any("error", err)).Error("Database error", logger.Any("details", fields))
|
||||
case elapsed > l.Config.SlowThreshold && l.Config.LogLevel >= logger.Warn:
|
||||
l.logger.WithContext(ctx).Warn("Slow database query", logger.Any("details", fields))
|
||||
case l.Config.LogLevel == logger.Info:
|
||||
l.logger.WithContext(ctx).Debug("Database query", logger.Any("details", fields))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Metode Koneksi GORM (diperbaiki untuk menggunakan adapter) ---
|
||||
|
||||
func (s *service) openPostgresGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Jakarta",
|
||||
config.Host, config.Username, config.Password, config.Database, config.Port, config.SSLMode)
|
||||
|
||||
if config.Schema != "" {
|
||||
dsn += fmt.Sprintf(" search_path=%s", config.Schema)
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
if config.RequireSSL {
|
||||
// ... (konfigurasi SSL tetap sama)
|
||||
}
|
||||
|
||||
return gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openMySQLGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||
|
||||
if config.RequireSSL {
|
||||
dsn += "&tls=true"
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(mysql.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openSQLServerGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s",
|
||||
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||
|
||||
if config.RequireSSL {
|
||||
// ... (konfigurasi SSL tetap sama)
|
||||
}
|
||||
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(sqlserver.Open(dsn), gormConfig)
|
||||
}
|
||||
|
||||
func (s *service) openSQLiteGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||
// PERBAIKAN: Gunakan adapter logger kita
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||
}
|
||||
|
||||
return gorm.Open(sqlite.Open(config.Path), gormConfig)
|
||||
}
|
||||
|
||||
// --- Metode Lainnya ---
|
||||
|
||||
// ... (semua metode lain seperti GetGormDB, GetDB, Migrate, Health, Close, dll. tidak perlu diubah)
|
||||
// ... (kecuali mungkin bagian Close yang membersihkan listeners)
|
||||
|
||||
// Di fungsi Close, pastikan tipe listener sudah benar
|
||||
func (s *service) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var errs []error
|
||||
|
||||
for name, listener := range s.listeners {
|
||||
if err := listener.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to close listener for %s: %w", name, err))
|
||||
}
|
||||
}
|
||||
// ... (sisa kode Close)
|
||||
}
|
||||
|
||||
// Di fungsi ListenForChanges, pastikan Anda menggunakan pq.NewListener
|
||||
func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error {
|
||||
// ... (kode Anda sudah benar menggunakan pq.NewListener)
|
||||
// ...
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
h1:w6tFa/NMDQdQDXl91m3KqSaPSa9ktGQH7la07Gu6eoc=
|
||||
20250904105930.sql h1:MEM6blCgke9DzWQSTnLzasbPIrcHssNNrJqZpSkEo6k=
|
||||
20250904141448.sql h1:J8cmYNk4ZrG9fhfbi2Z1IWz7YkfvhFqTzrLFo58BPY0=
|
||||
20250908062237.sql h1:Pu23yEW/aKkwozHoOuROvHS/GK4ngARJGdO7FB7HZuI=
|
||||
20250908062323.sql h1:oXl6Z143tOpIl4EfP4B8JNU8LrMvVmHEtCgAfiB4gs8=
|
||||
20250908073811.sql h1:m2aNXfnGxnLq1+rVWrh4f60q7fhyhV3gEwNu/OIqQlE=
|
||||
20250908073839.sql h1:cPk54xjLdMs26uY8ZHjNWLuyfAMzV7Zb0/9oJQrsw04=
|
||||
20250910055902.sql h1:5xwjAV6QbtZT9empTJKfhyAjdknbHzb15B0Ku5dzqtQ=
|
||||
20250915123412.sql h1:D83xaU2YlDEd21HLup/YQpQ2easMToYCyy/oK6AFgQs=
|
||||
20250916043819.sql h1:ekoTJsBqQZ8G8n0qJ03d13+eoNoc7sAUEQGA5D/CCxk=
|
||||
20250917040616.sql h1:zoCnmcXuM7AVv85SmN7RmFglCgJnoDmpRWExH0LAc9Q=
|
||||
20250917040751.sql h1:J1xyRrh32y1+lezwAyNwPcUQ6ABBSgbvzNLva4SVdQU=
|
||||
20250917045138.sql h1:jKe1Z0uOLG4SGBYM+S/3P+/zMPztmgoderD5swnMuCg=
|
||||
20250917093645.sql h1:cNI3Pbz1R3LxvIXLuexafJFCXUXrmuFCgXXJ2sG+FW0=
|
||||
20250918073552.sql h1:RJ1SvMzP6aeWnoPVD3eVAmIQOkcp6Php8z3QRri6v4g=
|
||||
20250918073742.sql h1:+cEsnJTJFybe2fR69ZoOiX2R6c6iITl4m6WTZ1hjyzY=
|
||||
20250918074745.sql h1:2hNVQCXF/dVYXAh+T/7oBFgERGWxzVb2FXJjwkFWGCI=
|
||||
20250923025134.sql h1:Ykz/qpHiGDXPsCsWTjydQFVSibZP2D+h2fIeb2h2JGA=
|
||||
20250924051317.sql h1:yQuW6SwJxIOM5fcxeAaie5lSm1oLysU/C2hH2xNCVoQ=
|
||||
20250929034321.sql h1:101FJ8VH12mrZWlt/X1gvKUGOhoiF8tFbjiapAjnHzg=
|
||||
20250929034428.sql h1:i+pROD9p+g5dOmmZma6WF/0Hw5g3Ha28NN85iTo1K34=
|
||||
20250930025550.sql h1:+F+CsCUXD/ql0tHGEow70GhPBX1ZybVn+bh/T4YMh7Y=
|
||||
20250930140351.sql h1:9AAEG1AnOAH+o0+oHL5G7I8vqlWOhwRlCGyyCpT/y1Q=
|
||||
20251002085604.sql h1:3xZ68eYp4urXRnvotNH1XvG2mYOSDV/j3zHEZ/txg5E=
|
||||
20251003032030.sql h1:HB+mQ2lXMNomHDpaRhB/9IwYI9/YiDO5eOJ+nAQH/jw=
|
||||
20251005060450.sql h1:LbtCE2b+8osM3CvnmQJH1uCPtn+d7WchsslBOz8bL3Q=
|
||||
20251006041122.sql h1:MlS7f21z06sutnf9dIekt5fuHJr4lgcQ4uCuCXAGsfc=
|
||||
20251006045658.sql h1:3FmGCPCzjgMPdWDRodZTsx3KVaodd9zB9ilib69aewk=
|
||||
20251006045928.sql h1:Z5g31PmnzNwk/OKdODcxZGm8fjJQdMFK32Xfnt3bRHg=
|
||||
20251007022859.sql h1:FO03zEfaNEk/aXwY81d5Lp3MoBB9kPQuXlXJ4BPiSR8=
|
||||
20251008031337.sql h1:l+sxUAGvcTfj3I6kAFHo+T6AYodC9k9GkR+jaKO2xXc=
|
||||
20251008031554.sql h1:AqrVfIhSzY3PCy8ZlP5W91wn2iznfIuj5qQfubp6/94=
|
||||
20251008052346.sql h1:nxnXmooIJ6r1mmzwnw+6efxLfc/k9h2aE6RMptPRons=
|
||||
20251008073620.sql h1:6YsJp1W4SmQJ1lxpqF27BBlDC1zqhw7Yhc7pLzQTY6M=
|
||||
20251009042854.sql h1:nkBV+R6j0fg7/JY6wH3eb5Vv0asJLnXmb6lINfT/GLQ=
|
||||
20251009052657.sql h1:EPvdsib5rzCGPryd10HShGKvFPwM/R5S2lIVwtYxpms=
|
||||
20251010031743.sql h1:T8IZmx8/btRFKLzTe78MzcBsPJNodnLvB0tby9QkirQ=
|
||||
20251010070721.sql h1:5NQUk/yOV6sABLCB7swx++YIOyJe6MnU+yt1nRzde5w=
|
||||
20251010072711.sql h1:ZJNqR2piyu8xJhBvVABSlnGEoKSKae3wuEs+wshPe4k=
|
||||
20251013044536.sql h1:0Xjw8fNILiT8nnfrJDZgQnPf3dntmIoilbapnih8AE4=
|
||||
20251013051438.sql h1:lfSuw5mgJnePBJamvhZ81osFIouXeiIEiSZ/evdwo48=
|
||||
20251013081808.sql h1:ijgjNX08G6GBjA/ks8EKtb7P7Y7Cg7zbhqEOruGnv6M=
|
||||
20251014060047.sql h1:0jqj49WTtneEIMQDBoo4c095ZGi8sCrA8NnHBrPU6D8=
|
||||
20251014063537.sql h1:VZLXol0PTsTW21Epg6vBPsztWkDtcxup9F/z88EGgIg=
|
||||
20251014063720.sql h1:2HVUyCV0ud3BJJDH2GEKZN/+IWLFPCsN1KqhP6csO14=
|
||||
20251015045455.sql h1:MeLWmMhAOAz8b15Dd7IAQnt6JxjSml02XCXK22C0Lpg=
|
||||
20251016010845.sql h1:4BncQdDOasRZJkzVJrSJJA7091A9VPNVx/faUCUPhBM=
|
||||
20251016011023.sql h1:9JB9eFZKURK5RoCVDKR6glSvdJ8NTXrN7K/4q51zkz4=
|
||||
20251016062912.sql h1:ACNn0fe+EMqUt3hoY+Dr3uqAV/QICBa1+mIW7fUc9Fk=
|
||||
20251017060617.sql h1:4T3t9ifWrEQTPMSM0XJ98pF7Qdt+UfgtMui17bhrnWI=
|
||||
20251017082207.sql h1:8vLG1l/saRRMHXkyA4nelJyjaSddhZd6r7R+Uo4JS/c=
|
||||
20251018032635.sql h1:2xey5gnO3y2XSOrU8MLlIfoylPKbRGDRtHDD07B3MbQ=
|
||||
20251018040322.sql h1:k/pdNiSoT8zFPqNQ/avOD0vYkNh3BTD64IlHrfVXr7I=
|
||||
20251019093915.sql h1:hFcQE0y+p5dZiVwePGsRGto9m/q6kJNiUZbVDd5Rnjk=
|
||||
20251020062553.sql h1:Iw7hulcm5iRQlfW+ygA4iTPxLqkxx6h9vXMXEwUAHKs=
|
||||
20251021041042.sql h1:wMgSivBV2A0NDcsLmKGIp0kMcVh2IODSG9b4dgzCaOM=
|
||||
20251021075552.sql h1:8gfSMAglflNO6L0sSzxFNEubYN8/O4thT7OQT+WH+3M=
|
||||
20251023044432.sql h1:MkvajJs3bfk9+wHvQ43/ccAluJEBARm1gWr1u92ccLA=
|
||||
20251024034832.sql h1:x3s3VEVYLOSKLAFxJGb2+c1FyTMMvPE+9k4Ew7rKQaI=
|
||||
20251024074315.sql h1:EjAjelgi5qAfcRq/8vPTlGGYHvAKxNTllm8f0SzZDns=
|
||||
20251025013451.sql h1:6hnuIiwYiG+6nLhOY/+Yyn+I6ZCFNRZxrJNqBV6HLqE=
|
||||
20251025013609.sql h1:evPJaTD8WxYRMOJZHkSr7ONLx9PYxT+ankzQt9c/sJ0=
|
||||
20251027075128.sql h1:/iFQBM1sytjqpyQSOx61q33gnorMgxTiFVSuL6bQqsM=
|
||||
20251027091406.sql h1:eCZGtUkxAzEAqpC9UsGpP8Df9mS0DEOqSl885LgqpvM=
|
||||
20251102002037.sql h1:lFJbuoZ2LMQnUNGdcwHVY3Xlfslgzu9t2WByT8yfOZI=
|
||||
20251102091932.sql h1:rmdhb5m+P+fU8jROBZNyeYgZKuQvucsuljXv4ZVzvks=
|
||||
20251103081637.sql h1:tf3BcwTeIw+oxMEisKDDfyKnBfalTLs8b0PJA8JWYxY=
|
||||
20251104042334.sql h1:7PDMWOhmJywolAPKFZ14XaDBeMvcxShaXFN2IemNtzk=
|
||||
20251104043530.sql h1:qvYVp3ysPf27f1BcoRNCFGovxuVE12lg9d6Xzda6zWU=
|
||||
20251104080952.sql h1:avghpv1n3yaCDR/TA0X+hgxDGoLBQGu/GJUwj4VT/Ic=
|
||||
20251104084135.sql h1:rg+eRE5/5sYWR7z+Xyn0zKw8rr8P/oWxF0xhcNVnNec=
|
||||
20251105044629.sql h1:4NU27HeKUNFsV82LacnwmnCSAH0pSbZR9J9/ZESRs6M=
|
||||
20251105121808.sql h1:fii6LjqWYjrm/pEIqttfvJI6QEUL49gque8wYHh1+yI=
|
||||
20251106035305.sql h1:oQ7BwnxPuwY2q98adIVc+lNwL/Sz1OceLJeClDo9/TI=
|
||||
20251106040137.sql h1:ppcqkVoT0o9jZcjI/TN7LuaPxXhJQhnIXEJtloP/46o=
|
||||
20251106041333.sql h1:2JkxyelQ/EeB+boL5bfpnzefw32ttEGKvKchtQjWmAU=
|
||||
20251106042006.sql h1:ruppYa1kAJQUU3ufQBbKGMcXrGbGJJiRPclT+dNc/YQ=
|
||||
20251106050412.sql h1:MiEMJ1HCFYnalKuq3Z38xJeogfBAMqsTv2sG4EF8dDw=
|
||||
20251106063418.sql h1:y3veDJPjKekOWLCZek/LgQwXPRhZtOppTfUXiqoL95s=
|
||||
20251106071906.sql h1:/TUZA3XpMY23qEJXdkTwlzrNMvSSl6JJniPcgAttBaw=
|
||||
20251106073157.sql h1:78txeibJ602DMD7huD618ZSMt6phSRzDNPTlo0PGyrc=
|
||||
20251106074218.sql h1:8Xz7WywrtUnSxOHhlal53gG9rE7r86LFUt5zBFe/mIs=
|
||||
20251106081846.sql h1:jp91Bf5bxGXMiUB1VIuN6y768vb2iWwow44WfCE5J5k=
|
||||
20251106082844.sql h1:RHYzRO4G1fSWwf+xc/3QezZ/Iil67cZPIgNpNz3TNhQ=
|
||||
20251106090021.sql h1:dFDk6mq+zjbYWmfWIrHf9DiKvvoXHjrr0++zssMTWP8=
|
||||
20251106144745.sql h1:aHcr23iBFqCHer5D/SsPMXBCLjGqUYvWYfRU8jSJgIw=
|
||||
20251107012049.sql h1:hu/7NHhnAkT4xK0RNtqmMDdH1Bo5EZbl7itDRjiCT+g=
|
||||
20251107064812.sql h1:sfCXDQYnMf0ddrQ9oYljWJLLSt9NJjJV6o8VS3p7aZE=
|
||||
20251107064937.sql h1:DlYGJ9LZFwZyR7jBP5zaGB128aIc4HAixBKPYCz9EkY=
|
||||
20251107071420.sql h1:ynCdZAd2utLl+FhtWZwtahNXgIVOvuk3s/rOq7lfXA4=
|
||||
20251107074318.sql h1:WE9cPhibWtZ0dbu1VEGirTeY6ijFYGMNhHdBtM32kOc=
|
||||
20251107075050.sql h1:8tvneruqdynDOaJK1+0z4CH7YXZStZpGdqwIeOMLik4=
|
||||
20251107080604.sql h1:8c4jd4Tql7tcdhbI9NS0tgvN+ADu9FnCf8wMUbmW7A0=
|
||||
20251107081830.sql h1:SAAe3lmsm9vGXuSEsDdl7ad0EAxP5CMmFRDEgp9M7yY=
|
||||
20251107091033.sql h1:JLdX/u7GUdBfjrPrMSNAqc8HtSoj0YA9iW9Vc6FJZdw=
|
||||
20251107091209.sql h1:CzhYtwAwT+GHrbqcagnJE+v3mbl/rObf1IJaLCKlzrs=
|
||||
20251107091541.sql h1:+3ZyWJTftDY2JeWThXuIxGWpUBnyMPyOyY4jBjdWYJI=
|
||||
20251110012217.sql h1:f4Z8TuGc+XMSJ+Ekn4/PeHRE2FlHWkc5gKPJB0hAX7c=
|
||||
20251110012306.sql h1:ENPyI6Kjdk6qKJQb0yJ6MCTDPAmO1WD/uhKuCSl+jYo=
|
||||
20251110052049.sql h1:OrQ0acnyoQLKnTitZfnBcVr5jDslF59OFLaqT7SpdVs=
|
||||
20251110062042.sql h1:9KwldQt0NpVPR86L0T4hlkfHAGau+7CiZYgu5rF+yhg=
|
||||
20251110063202.sql h1:A117DuZmZ6U0jWHA3DISnr+yvBjKIr1ObrUr047YezQ=
|
||||
20251110063633.sql h1:qTiC0F19JnhUIXF4LGJQ21jEV6kKGyhTr1x2kimFqPQ=
|
||||
20251110085551.sql h1:HZcJM0RSC6HBaUSjKBE8MgDG8Vn9f3LmwA/OnT9Cp7I=
|
||||
20251110091516.sql h1:W3AQhQLgirEWuCObbLl+Prdrbq6k6EEY1xcoWsmbog4=
|
||||
20251110091948.sql h1:3tsITMrZr/T+L4wqUMz8sHS229jCJl4T0Nu3dMccxH8=
|
||||
20251110092729.sql h1:uU+k88RH/e0Ns4/SmJl03RVYPscBAPuiLfxR6CJqaf0=
|
||||
20251110093522.sql h1:O7upSj8VNjzvroL4IU59bfxKATOkAVGBArcUbVNq9aM=
|
||||
20251110100258.sql h1://JSArUMNI3/gAtYDx2VN94C198SFW0ANjgs+p6eCRM=
|
||||
20251110100545.sql h1:ENPOqeJYRpMI4e8VCKwaQgaql8se6pIidAhG2cjskBg=
|
||||
20251110155448.sql h1:VW9KE0jxWy4flZ28sdXmhY6JWd6eKAX/cVL8KWRVii4=
|
||||
20251111072601.sql h1:99WWzGjcDDFNC2cHRfPu4MBLWNrgPMY5HAYUbmIcVRA=
|
||||
20251111073546.sql h1:iIGwFNfUgStdczw/PXTH3SD84xAyxrbZICofc3M8TMk=
|
||||
20251111074148.sql h1:mzkezSKwCV2bTZ/0BHiTCX5qAy4uHpwar1xzwAH15Ko=
|
||||
20251111074652.sql h1:lYh4/3BHV24pgPC0pP4RUKb+XtL/6AsZGPItRpnzBiQ=
|
||||
20251111082257.sql h1:+cIZ1lf8HYGSL6t6U88plLKk8nOO1YVdV7h3YOM84ds=
|
||||
20251111111017.sql h1:xzlhR2xLfOj4ddYlesS+bpEeDrxiAf5ILNOspsbZjBU=
|
||||
20251113101344.sql h1:vSzThY3p6XYTj0dJ2uFVkHmlytyrFAnGE58CJLx364I=
|
||||
20251113120533.sql h1:lfjgdqRGo/EohrVw8sWlFbDjh3ASTsfQ6pr3qjafqvc=
|
||||
20251114062746.sql h1:6DLYjfJ60PkAABZTXvOwSE+xrU25oyoX7gpYlBnA9cw=
|
||||
20251117005942.sql h1:0XoJKca8IOaB9QKFVF/qPY7jKcIgh6m/LODQuE06SAs=
|
||||
20251117075427.sql h1:LhY2urosfoSu1/vkHmgsNP4JA4DuWBs9gL59yMqcF8M=
|
||||
20251118074929.sql h1:3RWBD6BziQVw6WSfthgoVuhTELHER9NrIuZm4hY/F1A=
|
||||
20251119063438.sql h1:EVTG3ZrHjCmM95YPASZTRPiwHrG6e33A2vO4hLSAaBQ=
|
||||
20251119065730.sql h1:s98wnZOCW6NbnwDS3H53XIQ60u6B9bDwBLNvy5+Rn64=
|
||||
20251119072302.sql h1:ZL+VHekLYqgNtOFKlj02WHrAk11dtTxQkmyTKE8IOzY=
|
||||
20251119072450.sql h1:SiJy2vpSvoPFw4J1SW7HjGSJzrqR62NsjRYAeabV+kc=
|
||||
20251120005512.sql h1:hJUaWXYJ3HiSquBTw8OKhymjA4O43ehAMGfiOY8W87Q=
|
||||
20251120074415.sql h1:dNmcqZHqfZwi/wtYvO/pSFgImN2scXL0p/7g0+HLp0s=
|
||||
20251121033803.sql h1:onlddYGo+tQdGaEdJPqdsx3sncGnwEvqMSCksF6vjjI=
|
||||
20251124071457.sql h1:ikQLIXFikUbkUd55uJBmEvqLGEC9t0db+Om4TQsWP7M=
|
||||
20251125125303.sql h1:OLKbNPO36AHtIDursW9AeBvf60sahQKC1iOjHmpx0MA=
|
||||
20251126064057.sql h1:6al3PTWbw/WGiBcrRrVWppOMLtG+eRaH/qSLbnmh1kQ=
|
||||
20251201081333.sql h1:dD+jcisoUYnxRYWdtVcnxbDeYjUW12/R/qarYQr+utg=
|
||||
20251201104439.sql h1:h4tz5uetbCKeOI3agSVmAeh6jUfECYy0LhQ7HooYhzg=
|
||||
20251201113804.sql h1:DdUzqfLdy4AUpVSRFDrIJXntGmtKgZrtvv8MAT4mMxs=
|
||||
20251201113858.sql h1:XSpicm4aWjrfeY5EYIiw8Ios9v7BGfzZJxpW/57Qwqs=
|
||||
20251201114751.sql h1:qx+ShdHLacVFcRwwGrW1zMKFHOjMGdnusk7CEfyV8ig=
|
||||
20251201114913.sql h1:6l/m2/6fmTIjFmKio0QmB8oTwBDCHTIkbuYTODQ7Gwk=
|
||||
20251202030445.sql h1:5+6ss5KdN6pll8Qtf0RGGyy/BURNO3dBgIBJOzDb624=
|
||||
20251202044430.sql h1:nJtlQDGj7ZJtJ07NYcF41JCn9ck50GyDPoitTOe8P/s=
|
||||
20251202063820.sql h1:dXGXAp2Aq1PUuwAT5FtBquZUeEv5Pdb81/bFt+KR274=
|
||||
20251202064000.sql h1:+W5j/AipU7qnFLCTaS7jVd/SIbIPo4k5rRScAvu0L44=
|
||||
20251202130629.sql h1:rB/+hjwgQnHoA/uOv4pJbB0iXAeaPMinlzRI/755raY=
|
||||
20251202160848.sql h1:KbTqHicChkfszdW9pQzoPlnvtECKDPmwpV3Rc7+5a8o=
|
||||
20251202180207.sql h1:4DMAex+Go7MuMt15Di/J1C/ag15KwgWW1u8KDFmbJ9M=
|
||||
20251202231005.sql h1:4GcnNRFZnjY1nASBj1IUkzLYCYIfnSE9s6kcWXZSiO8=
|
||||
20251203205052.sql h1:0TZ4D3YKO2lmOAa60nhkJAQSx8QHgd+Zj4R39bPdU8Y=
|
||||
20251205073858.sql h1:F4o2SDJ/deNjvKaimBGccZ9t4Fir01YocT6emhEoqiU=
|
||||
20251205211957.sql h1:rJLemdhIo7NQRgsl5DANVuz8pAee7HciyGVET7V/DMk=
|
||||
20251205214433.sql h1:1axU9bZaXW+sASNXQn08dKBX+fDEj1qHGiigEoJQXjY=
|
||||
20251205221124.sql h1:TEy3NdLtM8+BkHwVT11kKvX3V28PP1bDMocZ0404Tm4=
|
||||
20251206021053.sql h1:cEmwbMKPz8DY2OUsr3ym3blyp823418qMnx/AJFFYqY=
|
||||
20251207020537.sql h1:hCENQuZ6jXSp3LrmZ7FgLxvI1gJNgBa+F39/RosKfgk=
|
||||
20251207212015.sql h1:uhNmVl9qXZnqwy3GXfxh5JJjjXK4mztmL19ioezZwVQ=
|
||||
20251207221222.sql h1:QMxCwIwb4M9jA9z8BJ9aNsOcMVwuZ1ZSsx0FjOlQjek=
|
||||
20251209022744.sql h1:BBb5VV+hUGtKW+nZhu/C6OhBgiQhs84vMYfLM51V+VA=
|
||||
20251209025908.sql h1:42z/3wPoOt/nkS92xZ9PHeu7m0IWgVICETkf6Ugtx7w=
|
||||
20251209030538.sql h1:M2zy92ZTBTHLzbKs7cv1S5epLbJPQPVxME5E8nrPL2w=
|
||||
20251209051742.sql h1:AKCu0xgzUlH5MjjBQLiHq6Gw54HA0j++Eknj1i6FHAw=
|
||||
20251209064304.sql h1:MSMSfF2HVU7Qg/nyHmh89i7+oAj3V2wbPv/5AQcYNYM=
|
||||
20251209070128.sql h1:CU3U33jEhzNFa2yc7aBGHv/XcyFEHRkjHUJFLTKZRjc=
|
||||
20251209084929.sql h1:5H1z+0hOmDHQastabHLaxN5d6YWiD2p4+nUf1Fmthrc=
|
||||
20251210145148.sql h1:tAs3iNEHnASYEQ4/h8XDXmKPvVASgfP6oTjnFdmBqFA=
|
||||
20251211101547.sql h1:hGxvjf2XLoHJIQQlCoSOgqOrIkGjk1A7CLdDV1R4pgc=
|
||||
20251211113942.sql h1:uxNunwWePW7UpR+8KtdvqQ4mUofJascWOOUEyRCKIEI=
|
||||
20251216074834.sql h1:jja2bz2dtaC4327V2iqs9LP7MsW6ycz68BM0KPld9xs=
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
||||
# Flexible Database Seeder
|
||||
|
||||
A flexible and extensible database seeder for Go applications with GORM support.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Flexible Configuration**: Support for any table structure
|
||||
- 📊 **CSV Import**: Import data from CSV files with customizable mapping
|
||||
- 🔄 **Batch Processing**: Efficient bulk inserts with configurable batch sizes
|
||||
- 🧪 **Dry Run Mode**: Preview seeding without actual database changes
|
||||
- ✅ **Validation**: Validate CSV files and configurations before seeding
|
||||
- 📝 **Column Mapping**: Map CSV columns to struct fields flexibly
|
||||
- 🗂️ **Multiple Tables**: Support for all master data tables
|
||||
- 🔍 **Progress Tracking**: Detailed logging and error reporting
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build the Seeder CLI
|
||||
|
||||
```bash
|
||||
make seeder-build
|
||||
```
|
||||
|
||||
### List Available Tables
|
||||
|
||||
```bash
|
||||
make seeder-list
|
||||
# or
|
||||
./bin/seeder list
|
||||
```
|
||||
|
||||
### Seed a Specific Table
|
||||
|
||||
```bash
|
||||
make seeder-seed TABLE=province
|
||||
# or
|
||||
./bin/seeder seed province
|
||||
```
|
||||
|
||||
### Seed All Tables
|
||||
|
||||
```bash
|
||||
make seeder-seed-all
|
||||
# or
|
||||
./bin/seeder seed all
|
||||
```
|
||||
|
||||
### Dry Run (Preview)
|
||||
|
||||
```bash
|
||||
make seeder-dry-run TABLE=ethnic
|
||||
# or
|
||||
./bin/seeder dry-run ethnic
|
||||
```
|
||||
|
||||
### Validate Configuration
|
||||
|
||||
```bash
|
||||
make seeder-validate
|
||||
# or
|
||||
./bin/seeder validate all
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Command Line Options
|
||||
|
||||
```bash
|
||||
# Seed with custom batch size and delete existing data
|
||||
./bin/seeder seed province -batch-size=200 -delete-before
|
||||
|
||||
# Seed with custom CSV file path
|
||||
./bin/seeder seed province -csv-path=/path/to/custom/provinces.csv
|
||||
|
||||
# Seed with custom table name
|
||||
./bin/seeder seed province -table-name=provinces_backup
|
||||
|
||||
# Dry run with specific options
|
||||
./bin/seeder dry-run ethnic -batch-size=10
|
||||
```
|
||||
|
||||
### Makefile Targets
|
||||
|
||||
```bash
|
||||
# Advanced seeding with options
|
||||
make seeder-advanced TABLE=province BATCH_SIZE=200 DELETE_BEFORE=1
|
||||
|
||||
# Build seeder
|
||||
make seeder-build
|
||||
|
||||
# Clean seeder binary
|
||||
make clean-seeder
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Table Configuration
|
||||
|
||||
Each table has a configuration that includes:
|
||||
|
||||
- **TableName**: Database table name
|
||||
- **CSVFile**: Path to CSV file
|
||||
- **ColumnMap**: Mapping between CSV headers and struct fields
|
||||
- **DeleteBefore**: Whether to delete existing data before seeding
|
||||
- **BatchSize**: Number of records to insert per batch
|
||||
|
||||
### Default Registry
|
||||
|
||||
The seeder comes with pre-configured tables:
|
||||
|
||||
| Table | CSV File | Description |
|
||||
|-------|----------|-------------|
|
||||
| province | provinces.csv | Province data |
|
||||
| regency | regencies.csv | Regency/City data |
|
||||
| district | districts.csv | District data |
|
||||
| village | villages.csv | Village data |
|
||||
| ethnic | ethnics.csv | Ethnic groups |
|
||||
| language | languages.csv | Languages |
|
||||
| installation | installations.csv | Hospital installations |
|
||||
| unit | units.csv | Medical units |
|
||||
| specialist | specialists.csv | Medical specialists |
|
||||
| subspecialist | subspecialists.csv | Medical subspecialists |
|
||||
|
||||
### CSV File Format
|
||||
|
||||
CSV files should follow these conventions:
|
||||
|
||||
1. **Header Row**: First row must contain column names
|
||||
2. **Column Names**: Use snake_case or PascalCase
|
||||
3. **Data Types**: Automatic type conversion is supported
|
||||
4. **Empty Values**: Empty cells are handled gracefully
|
||||
|
||||
Example CSV format:
|
||||
```csv
|
||||
Code,Name,Status
|
||||
001,General Medicine,true
|
||||
002,Cardiology,true
|
||||
003,Neurology,true
|
||||
```
|
||||
|
||||
## Extending the Seeder
|
||||
|
||||
### Adding New Tables
|
||||
|
||||
1. **Create Entity Struct** (if not exists):
|
||||
```go
|
||||
type MyEntity struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
Status bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in DefaultRegistry**:
|
||||
```go
|
||||
registry.Register("mytable", TableConfig{
|
||||
TableName: "MyTable",
|
||||
CSVFile: filepath.Join(basePath, "mytable.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
```
|
||||
|
||||
3. **Add to GetEntityByTableName**:
|
||||
```go
|
||||
case "mytable":
|
||||
return &MyEntity{}
|
||||
```
|
||||
|
||||
### Custom Column Mapping
|
||||
|
||||
The seeder supports flexible column mapping:
|
||||
|
||||
```go
|
||||
ColumnMap: map[string]string{
|
||||
"CSV_Column_Name": "StructFieldName",
|
||||
"province_code": "ProvinceCode",
|
||||
"full_name": "Name",
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Type Conversion
|
||||
|
||||
For complex types, you can extend the `setFieldValue` function in `MasterSeeder`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The seeder provides detailed error messages for:
|
||||
|
||||
- **File Not Found**: CSV file doesn't exist
|
||||
- **Invalid CSV Format**: Malformed CSV files
|
||||
- **Database Errors**: Connection issues, constraint violations
|
||||
- **Type Conversion**: Invalid data types
|
||||
- **Validation**: Missing required fields
|
||||
|
||||
## Logging
|
||||
|
||||
The seeder provides comprehensive logging:
|
||||
|
||||
```
|
||||
2024-01-01 12:00:00 [INFO] Starting seed for table: province
|
||||
2024-01-01 12:00:00 [INFO] CSV file: internal/infrastructure/database/csv/provinces.csv
|
||||
2024-01-01 12:00:00 [INFO] Batch size: 50
|
||||
2024-01-01 12:00:00 [INFO] Delete before: true
|
||||
2024-01-01 12:00:00 [INFO] Seeded province: 11 - ACEH
|
||||
2024-01-01 12:00:00 [INFO] Seeding completed. 38 records processed from Province
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Batch Size**: Use larger batch sizes (100-500) for better performance
|
||||
2. **Indexing**: Consider dropping indexes during seeding for large datasets
|
||||
3. **Transactions**: Each batch is processed in a transaction
|
||||
4. **Dry Run**: Always use dry-run first to validate data
|
||||
5. **Delete Strategy**: Use `delete-before` for clean slate seeding
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Database connection (optional, defaults to localhost)
|
||||
export DATABASE_URL="host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **CSV File Not Found**
|
||||
```
|
||||
Error: CSV file not found: internal/infrastructure/database/csv/provinces.csv
|
||||
```
|
||||
Solution: Ensure CSV files are in the correct directory
|
||||
|
||||
2. **Database Connection Failed**
|
||||
```
|
||||
Error: Failed to connect to database: ...
|
||||
```
|
||||
Solution: Check DATABASE_URL environment variable
|
||||
|
||||
3. **Invalid Column Mapping**
|
||||
```
|
||||
Error: Failed to set field X: field not found
|
||||
```
|
||||
Solution: Check ColumnMap configuration
|
||||
|
||||
4. **Type Conversion Error**
|
||||
```
|
||||
Error: Failed to parse int from 'ABC': ...
|
||||
```
|
||||
Solution: Check CSV data types match expected format
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Use dry-run mode to preview data without inserting:
|
||||
```bash
|
||||
./bin/seeder dry-run province
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Add tests for new functionality
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This seeder is part of the service-general project and follows the same license terms.
|
||||
@@ -0,0 +1,399 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SeederCLI adalah command-line interface untuk seeder
|
||||
type SeederCLI struct {
|
||||
db *gorm.DB
|
||||
registry *SeederRegistry
|
||||
}
|
||||
|
||||
// NewSeederCLI membuat CLI baru
|
||||
func NewSeederCLI(db *gorm.DB) *SeederCLI {
|
||||
return &SeederCLI{
|
||||
db: db,
|
||||
registry: DefaultRegistry(),
|
||||
}
|
||||
}
|
||||
|
||||
// Run menjalankan CLI seeder
|
||||
func (c *SeederCLI) Run(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return c.showHelp()
|
||||
}
|
||||
|
||||
command := args[0]
|
||||
switch command {
|
||||
case "list":
|
||||
return c.listTables()
|
||||
case "seed":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder seed <table-name|all> [options]")
|
||||
}
|
||||
return c.seedTable(args[1], args[2:])
|
||||
case "dry-run":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder dry-run <table-name|all> [options]")
|
||||
}
|
||||
return c.dryRunTable(args[1], args[2:])
|
||||
case "validate":
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: seeder validate <table-name|all>")
|
||||
}
|
||||
return c.validateTable(args[1])
|
||||
default:
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
// showHelp menampilkan bantuan
|
||||
func (c *SeederCLI) showHelp() error {
|
||||
help := `
|
||||
Seeder CLI - Flexible Database Seeder
|
||||
|
||||
Usage:
|
||||
seeder <command> [arguments]
|
||||
|
||||
Commands:
|
||||
list Show all available tables
|
||||
seed <table|all> Seed data for specific table or all tables
|
||||
dry-run <table|all> Preview seeding without actual insertion
|
||||
validate <table|all> Validate CSV files and configuration
|
||||
|
||||
Examples:
|
||||
seeder list
|
||||
seeder seed province
|
||||
seeder seed all
|
||||
seeder dry-run ethnic
|
||||
seeder validate regency
|
||||
|
||||
Options:
|
||||
-batch-size=N Set batch size for insertion (default: from config)
|
||||
-delete-before Delete existing data before seeding
|
||||
-no-delete Don't delete existing data (default)
|
||||
-csv-path=PATH Override CSV file path
|
||||
-table-name=NAME Override table name
|
||||
|
||||
Notes:
|
||||
- CSV files should be in internal/infrastructure/database/csv/
|
||||
- First row of CSV should be header with column names
|
||||
- Column names in CSV will be mapped to struct fields
|
||||
`
|
||||
fmt.Println(help)
|
||||
return nil
|
||||
}
|
||||
|
||||
// listTables menampilkan daftar tabel yang tersedia
|
||||
func (c *SeederCLI) listTables() error {
|
||||
tables := c.registry.List()
|
||||
|
||||
fmt.Println("Available tables for seeding:")
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
for _, table := range tables {
|
||||
config, _ := c.registry.Get(table)
|
||||
fmt.Printf("%-20s %s\n", table, config.CSVFile)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTotal: %d tables\n", len(tables))
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedTable melakukan seeding untuk tabel tertentu
|
||||
func (c *SeederCLI) seedTable(tableName string, options []string) error {
|
||||
if tableName == "all" {
|
||||
return c.seedAllTables(options)
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Parse options
|
||||
opts := c.parseOptions(options)
|
||||
|
||||
// Override config dengan options
|
||||
if opts.CSVPath != "" {
|
||||
config.CSVFile = opts.CSVPath
|
||||
}
|
||||
if opts.TableName != "" {
|
||||
config.TableName = opts.TableName
|
||||
}
|
||||
if opts.BatchSize > 0 {
|
||||
config.BatchSize = opts.BatchSize
|
||||
}
|
||||
if opts.DeleteBefore {
|
||||
config.DeleteBefore = true
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Create seeder
|
||||
seederConfig := SeederConfig{
|
||||
CSVPath: config.CSVFile,
|
||||
TableName: config.TableName,
|
||||
ColumnMap: config.ColumnMap,
|
||||
SkipHeader: true,
|
||||
BatchSize: config.BatchSize,
|
||||
DeleteBefore: config.DeleteBefore,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||
|
||||
log.Printf("Starting seed for table: %s", config.TableName)
|
||||
log.Printf("CSV file: %s", config.CSVFile)
|
||||
log.Printf("Batch size: %d", config.BatchSize)
|
||||
log.Printf("Delete before: %v", config.DeleteBefore)
|
||||
|
||||
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||
return fmt.Errorf("seeding failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully seeded table: %s", config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// dryRunTable melakukan dry-run untuk tabel tertentu
|
||||
func (c *SeederCLI) dryRunTable(tableName string, options []string) error {
|
||||
if tableName == "all" {
|
||||
return c.dryRunAllTables(options)
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Parse options
|
||||
opts := c.parseOptions(options)
|
||||
|
||||
// Override config dengan options
|
||||
if opts.CSVPath != "" {
|
||||
config.CSVFile = opts.CSVPath
|
||||
}
|
||||
if opts.TableName != "" {
|
||||
config.TableName = opts.TableName
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Create seeder dengan dry-run mode
|
||||
seederConfig := SeederConfig{
|
||||
CSVPath: config.CSVFile,
|
||||
TableName: config.TableName,
|
||||
ColumnMap: config.ColumnMap,
|
||||
SkipHeader: true,
|
||||
BatchSize: 10, // Smaller batch for dry-run
|
||||
DeleteBefore: false,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||
|
||||
log.Printf("Starting dry-run for table: %s", config.TableName)
|
||||
log.Printf("CSV file: %s", config.CSVFile)
|
||||
|
||||
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||
return fmt.Errorf("dry-run failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Dry-run completed for table: %s", config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTable memvalidasi konfigurasi dan CSV file
|
||||
func (c *SeederCLI) validateTable(tableName string) error {
|
||||
if tableName == "all" {
|
||||
return c.validateAllTables()
|
||||
}
|
||||
|
||||
config, exists := c.registry.Get(tableName)
|
||||
if !exists {
|
||||
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||
}
|
||||
|
||||
// Validasi config
|
||||
if err := config.Validate(); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Get entity
|
||||
entity := GetEntityByTableName(config.TableName)
|
||||
if entity == nil {
|
||||
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||
}
|
||||
|
||||
// Baca CSV file dan validasi
|
||||
file, err := os.Open(config.CSVFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
headers, err := reader.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||
}
|
||||
|
||||
// Hitung jumlah baris
|
||||
rowCount := 0
|
||||
for {
|
||||
_, err := reader.Read()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
rowCount++
|
||||
}
|
||||
|
||||
fmt.Printf("Validation passed for table: %s\n", tableName)
|
||||
fmt.Printf("CSV file: %s\n", config.CSVFile)
|
||||
fmt.Printf("Headers: %v\n", headers)
|
||||
fmt.Printf("Expected rows: %d\n", rowCount)
|
||||
fmt.Printf("Column mapping: %v\n", config.ColumnMap)
|
||||
fmt.Printf("Entity type: %T\n", entity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedAllTables melakukan seeding untuk semua tabel
|
||||
func (c *SeederCLI) seedAllTables(options []string) error {
|
||||
tables := c.registry.List()
|
||||
|
||||
log.Printf("Starting seed for all tables (%d tables)", len(tables))
|
||||
|
||||
for _, table := range tables {
|
||||
if err := c.seedTable(table, options); err != nil {
|
||||
log.Printf("Error seeding table %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Completed seeding all tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
// dryRunAllTables melakukan dry-run untuk semua tabel
|
||||
func (c *SeederCLI) dryRunAllTables(options []string) error {
|
||||
tables := c.registry.List()
|
||||
|
||||
log.Printf("Starting dry-run for all tables (%d tables)", len(tables))
|
||||
|
||||
for _, table := range tables {
|
||||
if err := c.dryRunTable(table, options); err != nil {
|
||||
log.Printf("Error in dry-run for table %s: %v", table, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Completed dry-run for all tables")
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAllTables memvalidasi semua tabel
|
||||
func (c *SeederCLI) validateAllTables() error {
|
||||
tables := c.registry.List()
|
||||
|
||||
fmt.Printf("Validating all tables (%d tables):\n", len(tables))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
for _, table := range tables {
|
||||
fmt.Printf("\n[%s]\n", table)
|
||||
if err := c.validateTable(table); err != nil {
|
||||
fmt.Printf("❌ Validation failed: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("✅ Validation passed\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeederOptions berisi parsed options
|
||||
type SeederOptions struct {
|
||||
CSVPath string
|
||||
TableName string
|
||||
BatchSize int
|
||||
DeleteBefore bool
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// parseOptions mem-parsing command line options
|
||||
func (c *SeederCLI) parseOptions(options []string) SeederOptions {
|
||||
var opts SeederOptions
|
||||
|
||||
for _, option := range options {
|
||||
switch {
|
||||
case strings.HasPrefix(option, "-batch-size="):
|
||||
fmt.Sscanf(option, "-batch-size=%d", &opts.BatchSize)
|
||||
case option == "-delete-before":
|
||||
opts.DeleteBefore = true
|
||||
case strings.HasPrefix(option, "-csv-path="):
|
||||
opts.CSVPath = strings.TrimPrefix(option, "-csv-path=")
|
||||
case strings.HasPrefix(option, "-table-name="):
|
||||
opts.TableName = strings.TrimPrefix(option, "-table-name=")
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// MainCLI adalah entry point utama untuk CLI
|
||||
func MainCLI() {
|
||||
// Setup database connection
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
cli := NewSeederCLI(db)
|
||||
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupDatabase setup koneksi database
|
||||
func setupDatabase() (*gorm.DB, error) {
|
||||
// Bisa diambil dari config atau environment variable
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
dsn = "host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TableConfig berisi konfigurasi untuk setiap tabel
|
||||
type TableConfig struct {
|
||||
TableName string
|
||||
CSVFile string
|
||||
Entity interface{}
|
||||
ColumnMap map[string]string
|
||||
DeleteBefore bool
|
||||
BatchSize int
|
||||
}
|
||||
|
||||
// SeederRegistry berisi registry untuk semua tabel yang bisa di-seed
|
||||
type SeederRegistry struct {
|
||||
tables map[string]TableConfig
|
||||
}
|
||||
|
||||
// NewSeederRegistry membuat registry baru
|
||||
func NewSeederRegistry() *SeederRegistry {
|
||||
return &SeederRegistry{
|
||||
tables: make(map[string]TableConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Register mendaftarkan tabel ke registry
|
||||
func (r *SeederRegistry) Register(name string, config TableConfig) {
|
||||
r.tables[name] = config
|
||||
}
|
||||
|
||||
// Get mendapatkan konfigurasi tabel berdasarkan nama
|
||||
func (r *SeederRegistry) Get(name string) (TableConfig, bool) {
|
||||
config, exists := r.tables[name]
|
||||
return config, exists
|
||||
}
|
||||
|
||||
// List mengembalikan daftar nama tabel yang terdaftar
|
||||
func (r *SeederRegistry) List() []string {
|
||||
names := make([]string, 0, len(r.tables))
|
||||
for name := range r.tables {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// DefaultRegistry berisi konfigurasi default untuk semua tabel
|
||||
func DefaultRegistry() *SeederRegistry {
|
||||
registry := NewSeederRegistry()
|
||||
|
||||
basePath := "internal/infrastructure/database/csv"
|
||||
|
||||
// Register Province
|
||||
registry.Register("province", TableConfig{
|
||||
TableName: "Province",
|
||||
CSVFile: filepath.Join(basePath, "provinces.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Regency
|
||||
registry.Register("regency", TableConfig{
|
||||
TableName: "Regency",
|
||||
CSVFile: filepath.Join(basePath, "regencies.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Province_Code": "ProvinceCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 100,
|
||||
})
|
||||
|
||||
// Register District
|
||||
registry.Register("district", TableConfig{
|
||||
TableName: "District",
|
||||
CSVFile: filepath.Join(basePath, "districts.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Regency_Code": "RegencyCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 200,
|
||||
})
|
||||
|
||||
// Register Village
|
||||
registry.Register("village", TableConfig{
|
||||
TableName: "Village",
|
||||
CSVFile: filepath.Join(basePath, "villages.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"District_Code": "DistrictCode",
|
||||
"Name": "Name",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 500,
|
||||
})
|
||||
|
||||
// Register Ethnic
|
||||
registry.Register("ethnic", TableConfig{
|
||||
TableName: "Ethnic",
|
||||
CSVFile: filepath.Join(basePath, "ethnics.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Language
|
||||
registry.Register("language", TableConfig{
|
||||
TableName: "Language",
|
||||
CSVFile: filepath.Join(basePath, "languages.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Installation
|
||||
registry.Register("installation", TableConfig{
|
||||
TableName: "Installation",
|
||||
CSVFile: filepath.Join(basePath, "installations.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Unit
|
||||
registry.Register("unit", TableConfig{
|
||||
TableName: "Unit",
|
||||
CSVFile: filepath.Join(basePath, "units.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register Specialist
|
||||
registry.Register("specialist", TableConfig{
|
||||
TableName: "Specialist",
|
||||
CSVFile: filepath.Join(basePath, "specialists.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register SubSpecialist
|
||||
registry.Register("subspecialist", TableConfig{
|
||||
TableName: "SubSpecialist",
|
||||
CSVFile: filepath.Join(basePath, "subspecialists.csv"),
|
||||
ColumnMap: map[string]string{
|
||||
"Code": "Code",
|
||||
"Specialist_Code": "SpecialistCode",
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 50,
|
||||
})
|
||||
|
||||
// Register RolPages (Menu Structure)
|
||||
registry.Register("rol_pages", TableConfig{
|
||||
TableName: "rol_pages",
|
||||
Entity: &RolPages{},
|
||||
DeleteBefore: true,
|
||||
BatchSize: 20,
|
||||
})
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// ValidateConfig memvalidasi konfigurasi
|
||||
func (c *TableConfig) Validate() error {
|
||||
if c.TableName == "" {
|
||||
return fmt.Errorf("table name is required")
|
||||
}
|
||||
if c.CSVFile == "" {
|
||||
return fmt.Errorf("CSV file path is required")
|
||||
}
|
||||
|
||||
// Cek apakah file CSV ada
|
||||
if _, err := os.Stat(c.CSVFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("CSV file not found: %s", c.CSVFile)
|
||||
}
|
||||
|
||||
if c.BatchSize <= 0 {
|
||||
c.BatchSize = 100
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEntityByTableName mendapatkan entity berdasarkan nama tabel
|
||||
func GetEntityByTableName(tableName string) interface{} {
|
||||
switch strings.ToLower(tableName) {
|
||||
case "province":
|
||||
return &Province{}
|
||||
case "regency":
|
||||
return &Regency{}
|
||||
case "district":
|
||||
return &District{}
|
||||
case "village":
|
||||
return &Village{}
|
||||
// case "ethnic":
|
||||
// return &Ethnic{}
|
||||
case "language":
|
||||
return &Language{}
|
||||
case "installation":
|
||||
return &Installation{}
|
||||
case "unit":
|
||||
return &Unit{}
|
||||
case "specialist":
|
||||
return &Specialist{}
|
||||
case "subspecialist":
|
||||
return &SubSpecialist{}
|
||||
case "rol_pages":
|
||||
return &RolPages{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Entity definitions
|
||||
|
||||
type Province struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Regency struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
ProvinceCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type District struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
RegencyCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Village struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
DistrictCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Installation struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Unit struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Specialist struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type SubSpecialist struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||
SpecialistCode interface{} `gorm:"index;not null"`
|
||||
Name interface{} `gorm:"not null"`
|
||||
Status interface{} `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
// RolPages struct untuk menu hierarchy
|
||||
type RolPages struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"not null;size:20"`
|
||||
Icon string `gorm:"size:100"`
|
||||
URL string `gorm:"not null"`
|
||||
Level int16 `gorm:"not null;default:0"`
|
||||
Sort int16 `gorm:"not null;default:0"`
|
||||
Parent *int64 `gorm:"index"`
|
||||
Active bool `gorm:"not null;default:true"`
|
||||
CreatedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP"`
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package seeders
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SeederConfig berisi konfigurasi untuk seeding
|
||||
type SeederConfig struct {
|
||||
CSVPath string
|
||||
TableName string
|
||||
ColumnMap map[string]string // mapping CSV header ke field struct
|
||||
SkipHeader bool
|
||||
BatchSize int
|
||||
DryRun bool
|
||||
DeleteBefore bool
|
||||
}
|
||||
|
||||
// MasterSeeder adalah struct utama untuk seeding fleksibel
|
||||
type MasterSeeder struct {
|
||||
db *gorm.DB
|
||||
config SeederConfig
|
||||
}
|
||||
|
||||
// NewMasterSeeder membuat instance seeder baru
|
||||
func NewMasterSeeder(db *gorm.DB, config SeederConfig) *MasterSeeder {
|
||||
if config.BatchSize == 0 {
|
||||
config.BatchSize = 100
|
||||
}
|
||||
return &MasterSeeder{
|
||||
db: db,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SeedFromCSV melakukan seeding dari file CSV
|
||||
func (s *MasterSeeder) SeedFromCSV(model interface{}) error {
|
||||
if s.config.CSVPath == "" {
|
||||
return fmt.Errorf("CSV path is required")
|
||||
}
|
||||
|
||||
file, err := os.Open(s.config.CSVPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.Comma = ','
|
||||
|
||||
// Baca header
|
||||
headers, err := reader.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("CSV Headers: %v", headers)
|
||||
|
||||
// Hapus data lama jika diminta
|
||||
if s.config.DeleteBefore {
|
||||
if err := s.db.Where("1=1").Delete(model).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete existing data: %w", err)
|
||||
}
|
||||
log.Printf("Deleted existing data from %s", s.config.TableName)
|
||||
}
|
||||
|
||||
count := 0
|
||||
batch := []interface{}{}
|
||||
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
log.Printf("Error reading row %d: %v", count+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(record) < len(headers) {
|
||||
log.Printf("Skipping incomplete row %d", count+1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Buat instance model baru
|
||||
instance := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
|
||||
// Mapping data dari CSV ke struct
|
||||
if err := s.mapCSVToStruct(headers, record, instance); err != nil {
|
||||
log.Printf("Error mapping row %d: %v", count+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set audit fields
|
||||
s.setAuditFields(instance)
|
||||
|
||||
if s.config.DryRun {
|
||||
log.Printf("Dry run - Would insert: %+v", instance)
|
||||
} else {
|
||||
batch = append(batch, instance)
|
||||
|
||||
// Insert batch jika sudah penuh
|
||||
if len(batch) >= s.config.BatchSize {
|
||||
if err := s.insertBatch(batch); err != nil {
|
||||
return fmt.Errorf("failed to insert batch: %w", err)
|
||||
}
|
||||
count += len(batch)
|
||||
batch = []interface{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert sisa batch
|
||||
if len(batch) > 0 && !s.config.DryRun {
|
||||
if err := s.insertBatch(batch); err != nil {
|
||||
return fmt.Errorf("failed to insert final batch: %w", err)
|
||||
}
|
||||
count += len(batch)
|
||||
}
|
||||
|
||||
log.Printf("Seeding completed. %d records processed from %s", count, s.config.TableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapCSVToStruct mapping data CSV ke struct berdasarkan column map atau reflection
|
||||
func (s *MasterSeeder) mapCSVToStruct(headers []string, record []string, instance interface{}) error {
|
||||
v := reflect.ValueOf(instance).Elem()
|
||||
t := v.Type()
|
||||
|
||||
for i, header := range headers {
|
||||
if i >= len(record) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(record[i])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Cari field berdasarkan column map atau nama header
|
||||
fieldName := s.getFieldName(header)
|
||||
|
||||
field, found := t.FieldByName(fieldName)
|
||||
if !found {
|
||||
// Coba dengan nama field yang berbeda case
|
||||
for j := 0; j < t.NumField(); j++ {
|
||||
f := t.Field(j)
|
||||
if strings.EqualFold(f.Name, fieldName) {
|
||||
field = f
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if err := s.setFieldValue(v.FieldByName(field.Name), value); err != nil {
|
||||
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFieldName mendapatkan nama field dari header CSV
|
||||
func (s *MasterSeeder) getFieldName(header string) string {
|
||||
// Cek di column map dulu
|
||||
if fieldName, exists := s.config.ColumnMap[header]; exists {
|
||||
return fieldName
|
||||
}
|
||||
|
||||
// Konversi header ke PascalCase
|
||||
words := strings.Split(header, "_")
|
||||
for i, word := range words {
|
||||
words[i] = strings.Title(strings.ToLower(word))
|
||||
}
|
||||
return strings.Join(words, "")
|
||||
}
|
||||
|
||||
// setFieldValue mengisi value ke field struct dengan type conversion
|
||||
func (s *MasterSeeder) setFieldValue(field reflect.Value, value string) error {
|
||||
if !field.CanSet() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(value)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
// Handle bigserial (int64)
|
||||
if value == "" {
|
||||
field.SetInt(0)
|
||||
} else {
|
||||
intVal, err := s.parseInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetInt(intVal)
|
||||
}
|
||||
case reflect.Bool:
|
||||
boolVal := strings.ToLower(value) == "true" || value == "1"
|
||||
field.SetBool(boolVal)
|
||||
case reflect.Interface:
|
||||
// Untuk field interface{} (seperti Code, Name, Status pada Ethnic)
|
||||
field.Set(reflect.ValueOf(value))
|
||||
default:
|
||||
// Handle pointer types
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if value == "" {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
} else {
|
||||
// Create new instance of the pointed type
|
||||
newValue := reflect.New(field.Type().Elem())
|
||||
if err := s.setFieldValue(newValue.Elem(), value); err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseInt parsing string ke int64
|
||||
func (s *MasterSeeder) parseInt(value string) (int64, error) {
|
||||
var intVal int64
|
||||
_, err := fmt.Sscanf(value, "%d", &intVal)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse int from '%s': %w", value, err)
|
||||
}
|
||||
return intVal, nil
|
||||
}
|
||||
|
||||
// setAuditFields mengisi field audit (CreatedAt, UpdatedAt, DeletedAt)
|
||||
func (s *MasterSeeder) setAuditFields(instance interface{}) {
|
||||
v := reflect.ValueOf(instance).Elem()
|
||||
now := time.Now()
|
||||
|
||||
// Set CreatedAt jika field ada
|
||||
if field := v.FieldByName("CreatedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Struct {
|
||||
// Handle time.Time
|
||||
field.Set(reflect.ValueOf(now))
|
||||
} else if field.Kind() == reflect.Interface {
|
||||
field.Set(reflect.ValueOf(now))
|
||||
}
|
||||
}
|
||||
|
||||
// Set UpdatedAt jika field ada (pointer)
|
||||
if field := v.FieldByName("UpdatedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
field.Set(reflect.ValueOf(&now))
|
||||
}
|
||||
}
|
||||
|
||||
// Set DeletedAt ke nil jika field ada
|
||||
if field := v.FieldByName("DeletedAt"); field.IsValid() && field.CanSet() {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insertBatch insert batch data ke database
|
||||
func (s *MasterSeeder) insertBatch(batch []interface{}) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range batch {
|
||||
// Gunakan FirstOrCreate untuk menghindari duplicate
|
||||
v := reflect.ValueOf(item).Elem()
|
||||
|
||||
// Buat kondisi where berdasarkan field utama (Code atau ID)
|
||||
var condition interface{}
|
||||
if codeField := v.FieldByName("Code"); codeField.IsValid() {
|
||||
condition = map[string]interface{}{"Code": codeField.Interface()}
|
||||
} else if idField := v.FieldByName("Id"); idField.IsValid() {
|
||||
condition = map[string]interface{}{"Id": idField.Interface()}
|
||||
}
|
||||
|
||||
if condition != nil {
|
||||
result := tx.Where(condition).FirstOrCreate(item)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
} else {
|
||||
// Jika tidak ada kondisi, insert langsung
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions untuk seeding spesifik
|
||||
|
||||
// SeedProvinces seed data provinsi dari CSV
|
||||
func SeedProvinces(db *gorm.DB, csvPath string) error {
|
||||
config := SeederConfig{
|
||||
CSVPath: csvPath,
|
||||
TableName: "Province",
|
||||
ColumnMap: map[string]string{"Code": "Code", "Name": "Name"},
|
||||
SkipHeader: true,
|
||||
BatchSize: 50,
|
||||
DeleteBefore: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(db, config)
|
||||
|
||||
// Definisikan struct untuk Province
|
||||
type Province struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
return seeder.SeedFromCSV(&Province{})
|
||||
}
|
||||
|
||||
// SeedRegencies seed data kabupaten dari CSV
|
||||
func SeedRegencies(db *gorm.DB, csvPath string) error {
|
||||
config := SeederConfig{
|
||||
CSVPath: csvPath,
|
||||
TableName: "Regency",
|
||||
ColumnMap: map[string]string{"Code": "Code", "Province_Code": "ProvinceCode", "Name": "Name"},
|
||||
SkipHeader: true,
|
||||
BatchSize: 100,
|
||||
DeleteBefore: true,
|
||||
}
|
||||
|
||||
seeder := NewMasterSeeder(db, config)
|
||||
|
||||
type Regency struct {
|
||||
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
ProvinceCode string `gorm:"index;not null"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
return seeder.SeedFromCSV(&Regency{})
|
||||
}
|
||||
|
||||
// SeedEthnics seed data suku bangsa dari CSV
|
||||
// func SeedEthnics(db *gorm.DB, csvPath string) error {
|
||||
// config := SeederConfig{
|
||||
// CSVPath: csvPath,
|
||||
// TableName: "Ethnic",
|
||||
// ColumnMap: map[string]string{"Code": "Code", "Name": "Name", "Status": "Status"},
|
||||
// SkipHeader: true,
|
||||
// BatchSize: 50,
|
||||
// DeleteBefore: true,
|
||||
// }
|
||||
|
||||
// seeder := NewMasterSeeder(db, config)
|
||||
|
||||
// // Gunakan struct Ethnic yang sudah ada
|
||||
// return seeder.SeedFromCSV(&Ethnic{})
|
||||
// }
|
||||
@@ -0,0 +1,50 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE SEQUENCE IF NOT EXISTS rol_pages_id_seq;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rol_pages (
|
||||
id int4 NOT NULL DEFAULT nextval('rol_pages_id_seq'),
|
||||
"name" varchar(20) NOT NULL,
|
||||
icon varchar(100) NULL,
|
||||
url text NOT NULL,
|
||||
"level" int2 NOT NULL DEFAULT 0,
|
||||
sort int2 NOT NULL DEFAULT 0,
|
||||
parent int4 NULL,
|
||||
active bool NOT NULL DEFAULT true,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NULL,
|
||||
deleted_at timestamp NULL,
|
||||
CONSTRAINT rol_pages_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT rol_pages_parent_fkey FOREIGN KEY (parent) REFERENCES rol_pages(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Index untuk performa query
|
||||
CREATE INDEX idx_rol_pages_parent ON rol_pages(parent) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_level ON rol_pages(level) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_active ON rol_pages(active) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_rol_pages_sort ON rol_pages(sort) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Trigger untuk update updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_rol_pages_updated_at BEFORE UPDATE ON rol_pages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS update_rol_pages_updated_at ON rol_pages;
|
||||
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||
DROP INDEX IF EXISTS idx_rol_pages_parent;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_level;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_active;
|
||||
DROP INDEX IF EXISTS idx_rol_pages_sort;
|
||||
DROP TABLE IF EXISTS rol_pages;
|
||||
DROP SEQUENCE IF EXISTS rol_pages_id_seq;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE rol_component (
|
||||
id int4 NOT NULL,
|
||||
"name" varchar(100) NULL,
|
||||
description text NULL,
|
||||
directory text NOT NULL,
|
||||
active bool NOT NULL,
|
||||
fk_rol_pages_id int4 NOT NULL,
|
||||
sort int2 NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE rol_permission (
|
||||
id int4 NOT NULL,
|
||||
"create" bool NULL,
|
||||
"read" bool NULL,
|
||||
"update" bool NULL,
|
||||
"disable" bool NULL,
|
||||
"delete" bool NULL,
|
||||
active bool NULL,
|
||||
fk_rol_pages_id int4 NULL,
|
||||
role_keycloak _text NULL,
|
||||
group_keycloak _text NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE role_access.rol_master (
|
||||
id int4 NOT NULL,
|
||||
"name" varchar(100) NULL,
|
||||
active bool NULL,
|
||||
created_at timestamp NULL,
|
||||
updated_at timestamp NULL,
|
||||
CONSTRAINT rol_master_role_pk PRIMARY KEY (id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,395 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/health.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// HealthMonitor handles health monitoring and metrics collection
|
||||
type HealthMonitor struct {
|
||||
metrics *Metrics
|
||||
dbService database.Service
|
||||
cacheManager *cache.Manager
|
||||
config *config.Config
|
||||
startTime time.Time
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewHealthMonitor creates a new health monitor
|
||||
func NewHealthMonitor(metrics *Metrics, dbService database.Service, cacheManager *cache.Manager, config *config.Config) *HealthMonitor {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "health_monitor"),
|
||||
logger.String("service", "goprint"),
|
||||
)
|
||||
|
||||
return &HealthMonitor{
|
||||
metrics: metrics,
|
||||
dbService: dbService,
|
||||
cacheManager: cacheManager,
|
||||
config: config,
|
||||
startTime: time.Now(),
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the health monitoring routine
|
||||
func (h *HealthMonitor) Start(ctx context.Context) {
|
||||
log := h.logger.WithFields(logger.String("action", "start_health_monitoring"))
|
||||
log.Info("Starting health monitoring service")
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Initial health check
|
||||
h.performHealthCheck()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
h.performHealthCheck()
|
||||
case <-ctx.Done():
|
||||
log.Info("Health monitoring service stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performHealthCheck performs comprehensive health check
|
||||
func (h *HealthMonitor) performHealthCheck() {
|
||||
log := h.logger.WithFields(logger.String("action", "health_check"))
|
||||
|
||||
// Check all databases
|
||||
dbHealthy := h.checkAllDatabases()
|
||||
|
||||
// Check cache
|
||||
cacheHealthy := h.checkCache()
|
||||
|
||||
// Check external services
|
||||
externalHealthy := h.checkExternalServices()
|
||||
|
||||
// Update service health
|
||||
overallHealthy := dbHealthy && cacheHealthy
|
||||
h.metrics.UpdateServiceHealth(overallHealthy)
|
||||
|
||||
// Update resource usage
|
||||
h.updateResourceUsage()
|
||||
|
||||
// Update uptime
|
||||
uptime := time.Since(h.startTime).Seconds()
|
||||
h.metrics.IncrementUptime(uptime)
|
||||
|
||||
log.Info("Health check completed",
|
||||
logger.Bool("database_healthy", dbHealthy),
|
||||
logger.Bool("cache_healthy", cacheHealthy),
|
||||
logger.Bool("external_healthy", externalHealthy),
|
||||
logger.Bool("overall_healthy", overallHealthy),
|
||||
logger.Float64("uptime_seconds", uptime),
|
||||
)
|
||||
}
|
||||
|
||||
// checkAllDatabases checks all configured databases
|
||||
func (h *HealthMonitor) checkAllDatabases() bool {
|
||||
log := h.logger.WithFields(logger.String("action", "check_databases"))
|
||||
|
||||
dbList := h.dbService.ListDBs()
|
||||
if len(dbList) == 0 {
|
||||
log.Warn("No databases configured")
|
||||
return false
|
||||
}
|
||||
|
||||
allHealthy := true
|
||||
for _, dbName := range dbList {
|
||||
if !h.checkDatabase(dbName) {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
return allHealthy
|
||||
}
|
||||
|
||||
// checkDatabase checks specific database connectivity
|
||||
func (h *HealthMonitor) checkDatabase(dbName string) bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_database"),
|
||||
logger.String("database", dbName),
|
||||
)
|
||||
|
||||
// Get database type
|
||||
dbType, err := h.dbService.GetDBType(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get database type", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check based on database type
|
||||
switch dbType {
|
||||
case database.Postgres, database.MySQL, database.SQLServer, database.SQLite:
|
||||
return h.checkSQLDatabase(dbName, log)
|
||||
case database.MongoDB:
|
||||
return h.checkMongoDatabase(dbName, log)
|
||||
default:
|
||||
log.Error("Unknown database type", logger.String("type", string(dbType)))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// checkSQLDatabase checks SQL database connectivity
|
||||
func (h *HealthMonitor) checkSQLDatabase(dbName string, log logger.Logger) bool {
|
||||
// Get GORM DB
|
||||
gormDB, err := h.dbService.GetGormDB(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get GORM database", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get SQL DB from GORM
|
||||
sqlDB, err := gormDB.DB()
|
||||
if err != nil {
|
||||
log.Error("Failed to get SQL database from GORM", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Test connection with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = sqlDB.PingContext(ctx)
|
||||
if err != nil {
|
||||
log.Error("Database ping failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(0)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get connection stats
|
||||
stats := sqlDB.Stats()
|
||||
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(float64(stats.OpenConnections))
|
||||
|
||||
log.Info("Database connection healthy",
|
||||
logger.Int("open_connections", stats.OpenConnections),
|
||||
logger.Int("idle_connections", stats.Idle),
|
||||
logger.Int("in_use_connections", stats.InUse),
|
||||
)
|
||||
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkMongoDatabase checks MongoDB connectivity
|
||||
func (h *HealthMonitor) checkMongoDatabase(dbName string, log logger.Logger) bool {
|
||||
client, err := h.dbService.GetMongoClient(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get MongoDB client", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Test connection with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = client.Ping(ctx, nil)
|
||||
if err != nil {
|
||||
log.Error("MongoDB ping failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Info("MongoDB connection healthy")
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkCache checks cache connectivity
|
||||
func (h *HealthMonitor) checkCache() bool {
|
||||
log := h.logger.WithFields(logger.String("action", "check_cache"))
|
||||
|
||||
if h.cacheManager == nil {
|
||||
log.Warn("Cache manager not available")
|
||||
return true // Cache is optional
|
||||
}
|
||||
|
||||
// Test cache health directly using manager
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := h.cacheManager.Health(ctx)
|
||||
if err != nil {
|
||||
log.Error("Cache health check failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus("redis", false)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Info("Cache connection healthy")
|
||||
h.metrics.UpdateExternalServiceStatus("redis", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkExternalServices checks external service connectivity
|
||||
func (h *HealthMonitor) checkExternalServices() bool {
|
||||
allHealthy := true
|
||||
|
||||
// Check BPJS service if configured
|
||||
if h.config != nil && h.config.Bpjs.BaseURL != "" {
|
||||
bpjsHealthy := h.checkBPJSService()
|
||||
if !bpjsHealthy {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check SatuSehat service if configured
|
||||
if h.config != nil && h.config.SatuSehat.BaseURL != "" {
|
||||
satuSehatHealthy := h.checkSatuSehatService()
|
||||
if !satuSehatHealthy {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
return allHealthy
|
||||
}
|
||||
|
||||
// checkBPJSService checks BPJS service connectivity
|
||||
func (h *HealthMonitor) checkBPJSService() bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_bpjs"),
|
||||
logger.String("service", "bpjs"),
|
||||
)
|
||||
|
||||
// Implement BPJS health check logic here
|
||||
// For now, return true as placeholder
|
||||
log.Info("BPJS service health check (placeholder)")
|
||||
h.metrics.UpdateExternalServiceStatus("bpjs", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkSatuSehatService checks SatuSehat service connectivity
|
||||
func (h *HealthMonitor) checkSatuSehatService() bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_satu_sehat"),
|
||||
logger.String("service", "satu_sehat"),
|
||||
)
|
||||
|
||||
// Implement SatuSehat health check logic here
|
||||
// For now, return true as placeholder
|
||||
log.Info("SatuSehat service health check (placeholder)")
|
||||
h.metrics.UpdateExternalServiceStatus("satu_sehat", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// updateResourceUsage updates resource usage metrics
|
||||
func (h *HealthMonitor) updateResourceUsage() {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
// Memory usage in MB
|
||||
memoryMB := float64(m.Alloc) / 1024 / 1024
|
||||
|
||||
// Number of goroutines
|
||||
goroutines := runtime.NumGoroutine()
|
||||
|
||||
// CPU usage would require more complex implementation
|
||||
// For now, we'll set it to 0 as placeholder
|
||||
cpuUsage := 0.0
|
||||
|
||||
h.metrics.UpdateResourceUsage(memoryMB, cpuUsage, goroutines)
|
||||
}
|
||||
|
||||
// RegisterHealthEndpoint registers health check endpoint with metrics
|
||||
func (h *HealthMonitor) RegisterHealthEndpoint(router *gin.Engine) {
|
||||
router.GET("/metrics/health", func(c *gin.Context) {
|
||||
health := gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC(),
|
||||
"uptime": time.Since(h.startTime).String(),
|
||||
"version": "1.0.0",
|
||||
"service": "service-general",
|
||||
}
|
||||
|
||||
// Check database
|
||||
dbHealthy := h.checkAllDatabases()
|
||||
health["database"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": dbHealthy,
|
||||
"databases": h.getDatabaseStatuses(),
|
||||
}
|
||||
|
||||
// Check cache
|
||||
cacheHealthy := h.checkCache()
|
||||
health["cache"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": cacheHealthy,
|
||||
}
|
||||
|
||||
// Check external services
|
||||
externalHealthy := h.checkExternalServices()
|
||||
health["external_services"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": externalHealthy,
|
||||
"services": h.getExternalServiceStatuses(),
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
overallHealthy := dbHealthy && cacheHealthy && externalHealthy
|
||||
if !overallHealthy {
|
||||
health["status"] = "unhealthy"
|
||||
c.JSON(503, health)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, health)
|
||||
})
|
||||
}
|
||||
|
||||
// getDatabaseStatuses returns detailed database statuses
|
||||
func (h *HealthMonitor) getDatabaseStatuses() map[string]interface{} {
|
||||
statuses := make(map[string]interface{})
|
||||
|
||||
dbList := h.dbService.ListDBs()
|
||||
for _, dbName := range dbList {
|
||||
dbType, _ := h.dbService.GetDBType(dbName)
|
||||
healthy := h.checkDatabase(dbName)
|
||||
|
||||
statuses[dbName] = gin.H{
|
||||
"type": string(dbType),
|
||||
"connected": healthy,
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
|
||||
// getExternalServiceStatuses returns external service statuses
|
||||
func (h *HealthMonitor) getExternalServiceStatuses() map[string]interface{} {
|
||||
statuses := make(map[string]interface{})
|
||||
|
||||
if h.config != nil {
|
||||
if h.config.Bpjs.BaseURL != "" {
|
||||
statuses["bpjs"] = gin.H{
|
||||
"connected": true, // Placeholder
|
||||
"url": h.config.Bpjs.BaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.SatuSehat.BaseURL != "" {
|
||||
statuses["satu_sehat"] = gin.H{
|
||||
"connected": true, // Placeholder
|
||||
"url": h.config.SatuSehat.BaseURL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/metrics.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// Metrics defines all Prometheus metrics for the GoPrint service
|
||||
type Metrics struct {
|
||||
// HTTP metrics
|
||||
HttpRequestsTotal *prometheus.CounterVec
|
||||
HttpRequestDuration *prometheus.HistogramVec
|
||||
HttpResponseSize *prometheus.HistogramVec
|
||||
HttpRequestsInFlight prometheus.Gauge
|
||||
|
||||
// Database metrics
|
||||
DbConnections prometheus.Gauge
|
||||
DbQueryDuration *prometheus.HistogramVec
|
||||
DbQueryErrors *prometheus.CounterVec
|
||||
DbActiveConnections *prometheus.GaugeVec
|
||||
|
||||
// Cache metrics
|
||||
CacheHits prometheus.Counter
|
||||
CacheMisses prometheus.Counter
|
||||
CacheOperations *prometheus.CounterVec
|
||||
CacheDuration *prometheus.HistogramVec
|
||||
|
||||
// Business logic metrics
|
||||
EthnicOperations *prometheus.CounterVec
|
||||
AuthOperations *prometheus.CounterVec
|
||||
PersonOperations *prometheus.CounterVec
|
||||
|
||||
// Service health metrics
|
||||
ServiceUptime prometheus.Counter
|
||||
ServiceHealthStatus prometheus.Gauge
|
||||
ExternalServiceStatus *prometheus.GaugeVec
|
||||
|
||||
// Resource usage metrics
|
||||
MemoryUsage prometheus.Gauge
|
||||
CPUUsage prometheus.Gauge
|
||||
Goroutines prometheus.Gauge
|
||||
|
||||
// Logger untuk monitoring
|
||||
logger logger.Logger
|
||||
serviceName string
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewMetrics creates a new instance of Prometheus metrics
|
||||
func NewMetrics(serviceName string) *Metrics {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "monitoring"),
|
||||
logger.String("service", serviceName),
|
||||
)
|
||||
|
||||
labels := []string{"method", "endpoint", "status"}
|
||||
dbLabels := []string{"operation", "table", "database"}
|
||||
cacheLabels := []string{"operation", "result"}
|
||||
businessLabels := []string{"operation", "status", "entity"}
|
||||
externalLabels := []string{"service", "status"}
|
||||
|
||||
metrics := &Metrics{
|
||||
serviceName: serviceName,
|
||||
logger: log,
|
||||
startTime: time.Now(),
|
||||
|
||||
// HTTP metrics
|
||||
HttpRequestsTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpRequestDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpResponseSize: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_response_size_bytes",
|
||||
Help: "HTTP response size in bytes",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.ExponentialBuckets(100, 10, 8),
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpRequestsInFlight: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "http_requests_in_flight",
|
||||
Help: "Number of HTTP requests currently being processed",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
|
||||
// Database metrics
|
||||
DbConnections: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "db_connections_active",
|
||||
Help: "Number of active database connections",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
DbActiveConnections: promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "db_connections_active_by_db",
|
||||
Help: "Number of active database connections by database",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
[]string{"database"},
|
||||
),
|
||||
DbQueryDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "db_query_duration_seconds",
|
||||
Help: "Database query duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
dbLabels,
|
||||
),
|
||||
DbQueryErrors: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "db_query_errors_total",
|
||||
Help: "Total number of database query errors",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
dbLabels,
|
||||
),
|
||||
|
||||
// Cache metrics
|
||||
CacheHits: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_hits_total",
|
||||
Help: "Total number of cache hits",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CacheMisses: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_misses_total",
|
||||
Help: "Total number of cache misses",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CacheOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_operations_total",
|
||||
Help: "Total number of cache operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
cacheLabels,
|
||||
),
|
||||
CacheDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "cache_operation_duration_seconds",
|
||||
Help: "Cache operation duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
cacheLabels,
|
||||
),
|
||||
|
||||
// Business logic metrics
|
||||
EthnicOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "ethnic_operations_total",
|
||||
Help: "Total number of ethnic operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
AuthOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "auth_operations_total",
|
||||
Help: "Total number of authentication operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
PersonOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "person_operations_total",
|
||||
Help: "Total number of person operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
|
||||
// Service health metrics
|
||||
ServiceUptime: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "service_uptime_seconds",
|
||||
Help: "Service uptime in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
ServiceHealthStatus: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "service_health_status",
|
||||
Help: "Service health status (1 = healthy, 0 = unhealthy)",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
ExternalServiceStatus: promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "external_service_status",
|
||||
Help: "External service status (1 = up, 0 = down)",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
externalLabels,
|
||||
),
|
||||
|
||||
// Resource usage metrics
|
||||
MemoryUsage: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "memory_usage_bytes",
|
||||
Help: "Memory usage in bytes",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CPUUsage: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "cpu_usage_percent",
|
||||
Help: "CPU usage percentage",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
Goroutines: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "goroutines_count",
|
||||
Help: "Number of goroutines",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
log.Info("Prometheus metrics initialized successfully",
|
||||
logger.String("service", serviceName),
|
||||
)
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// RecordHTTPRequest records HTTP request metrics
|
||||
func (m *Metrics) RecordHTTPRequest(method, endpoint, status string, duration time.Duration, responseSize int64) {
|
||||
labels := prometheus.Labels{
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
m.HttpRequestsTotal.With(labels).Inc()
|
||||
m.HttpRequestDuration.With(labels).Observe(duration.Seconds())
|
||||
m.HttpResponseSize.With(labels).Observe(float64(responseSize))
|
||||
|
||||
m.logger.Debug("HTTP request recorded",
|
||||
logger.String("method", method),
|
||||
logger.String("endpoint", endpoint),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.Int64("response_size", responseSize),
|
||||
)
|
||||
}
|
||||
|
||||
// RecordDBQuery records database query metrics
|
||||
func (m *Metrics) RecordDBQuery(operation, table, database string, duration time.Duration, err error) {
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
"database": database,
|
||||
}
|
||||
|
||||
m.DbQueryDuration.With(labels).Observe(duration.Seconds())
|
||||
if err != nil {
|
||||
m.DbQueryErrors.With(labels).Inc()
|
||||
m.logger.Error("Database query error recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("table", table),
|
||||
logger.String("database", database),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
} else {
|
||||
m.logger.Debug("Database query recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("table", table),
|
||||
logger.String("database", database),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordCacheOperation records cache operation metrics
|
||||
func (m *Metrics) RecordCacheOperation(operation string, hit bool, duration time.Duration) {
|
||||
if hit {
|
||||
m.CacheHits.Inc()
|
||||
} else {
|
||||
m.CacheMisses.Inc()
|
||||
}
|
||||
|
||||
result := "hit"
|
||||
if !hit {
|
||||
result = "miss"
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
m.CacheOperations.With(labels).Inc()
|
||||
m.CacheDuration.With(labels).Observe(duration.Seconds())
|
||||
|
||||
m.logger.Debug("Cache operation recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("result", result),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
)
|
||||
}
|
||||
|
||||
// RecordBusinessOperation records business logic operation metrics
|
||||
func (m *Metrics) RecordBusinessOperation(operationType, operation, status string) {
|
||||
var counter *prometheus.CounterVec
|
||||
|
||||
switch operationType {
|
||||
case "ethnic":
|
||||
counter = m.EthnicOperations
|
||||
case "auth":
|
||||
counter = m.AuthOperations
|
||||
case "person":
|
||||
counter = m.PersonOperations
|
||||
default:
|
||||
m.logger.Warn("Unknown business operation type",
|
||||
logger.String("type", operationType),
|
||||
logger.String("operation", operation),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"status": status,
|
||||
"entity": operationType,
|
||||
}
|
||||
|
||||
counter.With(labels).Inc()
|
||||
|
||||
m.logger.Debug("Business operation recorded",
|
||||
logger.String("type", operationType),
|
||||
logger.String("operation", operation),
|
||||
logger.String("status", status),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateServiceHealth updates service health status
|
||||
func (m *Metrics) UpdateServiceHealth(healthy bool) {
|
||||
if healthy {
|
||||
m.ServiceHealthStatus.Set(1)
|
||||
m.logger.Info("Service health status updated to healthy")
|
||||
} else {
|
||||
m.ServiceHealthStatus.Set(0)
|
||||
m.logger.Error("Service health status updated to unhealthy")
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateExternalServiceStatus updates external service status
|
||||
func (m *Metrics) UpdateExternalServiceStatus(service string, status bool) {
|
||||
value := 0.0
|
||||
statusStr := "down"
|
||||
if status {
|
||||
value = 1.0
|
||||
statusStr = "up"
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"service": service,
|
||||
"status": statusStr,
|
||||
}
|
||||
|
||||
m.ExternalServiceStatus.With(labels).Set(value)
|
||||
|
||||
m.logger.Info("External service status updated",
|
||||
logger.String("service", service),
|
||||
logger.String("status", statusStr),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateResourceUsage updates resource usage metrics
|
||||
func (m *Metrics) UpdateResourceUsage(memory, cpu float64, goroutines int) {
|
||||
m.MemoryUsage.Set(memory)
|
||||
m.CPUUsage.Set(cpu)
|
||||
m.Goroutines.Set(float64(goroutines))
|
||||
|
||||
m.logger.Debug("Resource usage updated",
|
||||
logger.Float64("memory_mb", memory),
|
||||
logger.Float64("cpu_percent", cpu),
|
||||
logger.Int("goroutines", goroutines),
|
||||
)
|
||||
}
|
||||
|
||||
// IncrementUptime increments service uptime
|
||||
func (m *Metrics) IncrementUptime(seconds float64) {
|
||||
m.ServiceUptime.Add(seconds)
|
||||
}
|
||||
|
||||
// GetUptime returns current uptime in seconds
|
||||
func (m *Metrics) GetUptime() float64 {
|
||||
return time.Since(m.startTime).Seconds()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/middleware.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MetricsMiddleware creates Gin middleware for collecting HTTP metrics
|
||||
func MetricsMiddleware(metrics *Metrics) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
// Add request ID to context for logging
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("request_id", requestID),
|
||||
logger.String("method", c.Request.Method),
|
||||
logger.String("path", c.Request.URL.Path),
|
||||
logger.String("client_ip", c.ClientIP()),
|
||||
)
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
|
||||
// Increment requests in flight
|
||||
metrics.HttpRequestsInFlight.Inc()
|
||||
defer metrics.HttpRequestsInFlight.Dec()
|
||||
|
||||
// Log request start
|
||||
log.Info("HTTP request started")
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Calculate duration
|
||||
duration := time.Since(start)
|
||||
|
||||
// Get response size
|
||||
responseSize := c.Writer.Size()
|
||||
if responseSize < 0 {
|
||||
responseSize = 0
|
||||
}
|
||||
|
||||
// Get status code
|
||||
status := strconv.Itoa(c.Writer.Status())
|
||||
|
||||
// Record metrics
|
||||
metrics.RecordHTTPRequest(
|
||||
c.Request.Method,
|
||||
c.FullPath(),
|
||||
status,
|
||||
duration,
|
||||
int64(responseSize),
|
||||
)
|
||||
|
||||
// Log request completion
|
||||
log.Info("HTTP request completed",
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.Int("response_size", responseSize),
|
||||
)
|
||||
|
||||
// Add request ID to response
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseMetricsMiddleware creates middleware for database metrics
|
||||
func DatabaseMetricsMiddleware(metrics *Metrics) func(operation, table, database string) func(error) {
|
||||
return func(operation, table, database string) func(error) {
|
||||
start := time.Now()
|
||||
return func(err error) {
|
||||
duration := time.Since(start)
|
||||
metrics.RecordDBQuery(operation, table, database, duration, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CacheMetricsMiddleware creates middleware for cache metrics
|
||||
func CacheMetricsMiddleware(metrics *Metrics) func(operation string, hit bool) func() {
|
||||
return func(operation string, hit bool) func() {
|
||||
start := time.Now()
|
||||
return func() {
|
||||
duration := time.Since(start)
|
||||
metrics.RecordCacheOperation(operation, hit, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/repository_wrapper.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormDBWrapper wraps GORM DB with metrics collection
|
||||
type GormDBWrapper struct {
|
||||
*gorm.DB
|
||||
metrics *Metrics
|
||||
database string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewGormDBWrapper creates a new GORM wrapper with metrics
|
||||
func NewGormDBWrapper(db *gorm.DB, metrics *Metrics, database string) *GormDBWrapper {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "gorm_wrapper"),
|
||||
logger.String("database", database),
|
||||
)
|
||||
|
||||
return &GormDBWrapper{
|
||||
DB: db,
|
||||
metrics: metrics,
|
||||
database: database,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Create overrides GORM Create with metrics
|
||||
func (w *GormDBWrapper) Create(value interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Create(value)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("create", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database create operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find overrides GORM Find with metrics
|
||||
func (w *GormDBWrapper) Find(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Find(dest, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("find", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database find operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// First overrides GORM First with metrics
|
||||
func (w *GormDBWrapper) First(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.First(dest, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("first", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database first operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Update overrides GORM Update with metrics
|
||||
func (w *GormDBWrapper) Update(column string, value interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Update(column, value)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("update", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database update operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete overrides GORM Delete with metrics
|
||||
func (w *GormDBWrapper) Delete(value interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Delete(value, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("delete", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database delete operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WithContext overrides GORM WithContext to maintain wrapper
|
||||
func (w *GormDBWrapper) WithContext(ctx context.Context) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.WithContext(ctx),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Model overrides GORM Model to maintain wrapper
|
||||
func (w *GormDBWrapper) Model(value interface{}) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.Model(value),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Table overrides GORM Table to maintain wrapper
|
||||
func (w *GormDBWrapper) Table(name string, args ...interface{}) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.Table(name, args...),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v3.12.4
|
||||
// source: internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto
|
||||
|
||||
package permissionV1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
wrappers "github.com/golang/protobuf/ptypes/wrappers"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Pesan request untuk RPC GetRolePermissionTree.
|
||||
type GetRolePermissionTreeRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RoleKeycloak string `protobuf:"bytes,1,opt,name=role_keycloak,json=roleKeycloak,proto3" json:"role_keycloak,omitempty"`
|
||||
GroupKeycloak []string `protobuf:"bytes,2,rep,name=group_keycloak,json=groupKeycloak,proto3" json:"group_keycloak,omitempty"`
|
||||
// Menggunakan wrapper BoolValue untuk membedakan antara 'false' dan 'tidak diset'.
|
||||
ActiveOnly *wrappers.BoolValue `protobuf:"bytes,3,opt,name=active_only,json=activeOnly,proto3" json:"active_only,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) Reset() {
|
||||
*x = GetRolePermissionTreeRequest{}
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetRolePermissionTreeRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetRolePermissionTreeRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetRolePermissionTreeRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) GetRoleKeycloak() string {
|
||||
if x != nil {
|
||||
return x.RoleKeycloak
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) GetGroupKeycloak() []string {
|
||||
if x != nil {
|
||||
return x.GroupKeycloak
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetRolePermissionTreeRequest) GetActiveOnly() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.ActiveOnly
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pesan response untuk RPC GetRolePermissionTree.
|
||||
type RolePermissionTreeResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
|
||||
Data *RolePermissionData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RolePermissionTreeResponse) Reset() {
|
||||
*x = RolePermissionTreeResponse{}
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RolePermissionTreeResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RolePermissionTreeResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RolePermissionTreeResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RolePermissionTreeResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RolePermissionTreeResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *RolePermissionTreeResponse) GetSuccess() bool {
|
||||
if x != nil {
|
||||
return x.Success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *RolePermissionTreeResponse) GetData() *RolePermissionData {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RolePermissionData struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"`
|
||||
Group []string `protobuf:"bytes,2,rep,name=group,proto3" json:"group,omitempty"`
|
||||
Access []*AccessTreeItem `protobuf:"bytes,3,rep,name=access,proto3" json:"access,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RolePermissionData) Reset() {
|
||||
*x = RolePermissionData{}
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RolePermissionData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RolePermissionData) ProtoMessage() {}
|
||||
|
||||
func (x *RolePermissionData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RolePermissionData.ProtoReflect.Descriptor instead.
|
||||
func (*RolePermissionData) Descriptor() ([]byte, []int) {
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *RolePermissionData) GetRole() string {
|
||||
if x != nil {
|
||||
return x.Role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RolePermissionData) GetGroup() []string {
|
||||
if x != nil {
|
||||
return x.Group
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RolePermissionData) GetAccess() []*AccessTreeItem {
|
||||
if x != nil {
|
||||
return x.Access
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AccessTreeItem struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"`
|
||||
Sort int32 `protobuf:"varint,6,opt,name=sort,proto3" json:"sort,omitempty"`
|
||||
Active bool `protobuf:"varint,7,opt,name=active,proto3" json:"active,omitempty"`
|
||||
Permission *Permission `protobuf:"bytes,8,opt,name=permission,proto3,oneof" json:"permission,omitempty"`
|
||||
Children []*AccessTreeItem `protobuf:"bytes,9,rep,name=children,proto3" json:"children,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) Reset() {
|
||||
*x = AccessTreeItem{}
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AccessTreeItem) ProtoMessage() {}
|
||||
|
||||
func (x *AccessTreeItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AccessTreeItem.ProtoReflect.Descriptor instead.
|
||||
func (*AccessTreeItem) Descriptor() ([]byte, []int) {
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetId() int64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetIcon() string {
|
||||
if x != nil {
|
||||
return x.Icon
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetUrl() string {
|
||||
if x != nil {
|
||||
return x.Url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetLevel() int32 {
|
||||
if x != nil {
|
||||
return x.Level
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetSort() int32 {
|
||||
if x != nil {
|
||||
return x.Sort
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetActive() bool {
|
||||
if x != nil {
|
||||
return x.Active
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetPermission() *Permission {
|
||||
if x != nil {
|
||||
return x.Permission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AccessTreeItem) GetChildren() []*AccessTreeItem {
|
||||
if x != nil {
|
||||
return x.Children
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Create bool `protobuf:"varint,1,opt,name=create,proto3" json:"create,omitempty"`
|
||||
Read bool `protobuf:"varint,2,opt,name=read,proto3" json:"read,omitempty"`
|
||||
Update bool `protobuf:"varint,3,opt,name=update,proto3" json:"update,omitempty"`
|
||||
Delete bool `protobuf:"varint,4,opt,name=delete,proto3" json:"delete,omitempty"`
|
||||
Disable bool `protobuf:"varint,5,opt,name=disable,proto3" json:"disable,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Permission) Reset() {
|
||||
*x = Permission{}
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Permission) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Permission) ProtoMessage() {}
|
||||
|
||||
func (x *Permission) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Permission.ProtoReflect.Descriptor instead.
|
||||
func (*Permission) Descriptor() ([]byte, []int) {
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *Permission) GetCreate() bool {
|
||||
if x != nil {
|
||||
return x.Create
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Permission) GetRead() bool {
|
||||
if x != nil {
|
||||
return x.Read
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Permission) GetUpdate() bool {
|
||||
if x != nil {
|
||||
return x.Update
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Permission) GetDelete() bool {
|
||||
if x != nil {
|
||||
return x.Delete
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Permission) GetDisable() bool {
|
||||
if x != nil {
|
||||
return x.Disable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"Kinternal/infrastructure/transport/grpc/proto/permission/v1/permission.proto\x12\rpermission.v1\x1a\x1egoogle/protobuf/wrappers.proto\"\xa7\x01\n" +
|
||||
"\x1cGetRolePermissionTreeRequest\x12#\n" +
|
||||
"\rrole_keycloak\x18\x01 \x01(\tR\froleKeycloak\x12%\n" +
|
||||
"\x0egroup_keycloak\x18\x02 \x03(\tR\rgroupKeycloak\x12;\n" +
|
||||
"\vactive_only\x18\x03 \x01(\v2\x1a.google.protobuf.BoolValueR\n" +
|
||||
"activeOnly\"m\n" +
|
||||
"\x1aRolePermissionTreeResponse\x12\x18\n" +
|
||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x125\n" +
|
||||
"\x04data\x18\x02 \x01(\v2!.permission.v1.RolePermissionDataR\x04data\"u\n" +
|
||||
"\x12RolePermissionData\x12\x12\n" +
|
||||
"\x04role\x18\x01 \x01(\tR\x04role\x12\x14\n" +
|
||||
"\x05group\x18\x02 \x03(\tR\x05group\x125\n" +
|
||||
"\x06access\x18\x03 \x03(\v2\x1d.permission.v1.AccessTreeItemR\x06access\"\xa6\x02\n" +
|
||||
"\x0eAccessTreeItem\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x10\n" +
|
||||
"\x03url\x18\x04 \x01(\tR\x03url\x12\x14\n" +
|
||||
"\x05level\x18\x05 \x01(\x05R\x05level\x12\x12\n" +
|
||||
"\x04sort\x18\x06 \x01(\x05R\x04sort\x12\x16\n" +
|
||||
"\x06active\x18\a \x01(\bR\x06active\x12>\n" +
|
||||
"\n" +
|
||||
"permission\x18\b \x01(\v2\x19.permission.v1.PermissionH\x00R\n" +
|
||||
"permission\x88\x01\x01\x129\n" +
|
||||
"\bchildren\x18\t \x03(\v2\x1d.permission.v1.AccessTreeItemR\bchildrenB\r\n" +
|
||||
"\v_permission\"\x82\x01\n" +
|
||||
"\n" +
|
||||
"Permission\x12\x16\n" +
|
||||
"\x06create\x18\x01 \x01(\bR\x06create\x12\x12\n" +
|
||||
"\x04read\x18\x02 \x01(\bR\x04read\x12\x16\n" +
|
||||
"\x06update\x18\x03 \x01(\bR\x06update\x12\x16\n" +
|
||||
"\x06delete\x18\x04 \x01(\bR\x06delete\x12\x18\n" +
|
||||
"\adisable\x18\x05 \x01(\bR\adisable2\x87\x01\n" +
|
||||
"\x14RolPermissionService\x12o\n" +
|
||||
"\x15GetRolePermissionTree\x12+.permission.v1.GetRolePermissionTreeRequest\x1a).permission.v1.RolePermissionTreeResponseBGZEinternal/infrastructure/transport/grpc/gen/permission/v1;permissionV1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescOnce sync.Once
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP() []byte {
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescOnce.Do(func() {
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc), len(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc)))
|
||||
})
|
||||
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes = []any{
|
||||
(*GetRolePermissionTreeRequest)(nil), // 0: permission.v1.GetRolePermissionTreeRequest
|
||||
(*RolePermissionTreeResponse)(nil), // 1: permission.v1.RolePermissionTreeResponse
|
||||
(*RolePermissionData)(nil), // 2: permission.v1.RolePermissionData
|
||||
(*AccessTreeItem)(nil), // 3: permission.v1.AccessTreeItem
|
||||
(*Permission)(nil), // 4: permission.v1.Permission
|
||||
(*wrappers.BoolValue)(nil), // 5: google.protobuf.BoolValue
|
||||
}
|
||||
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs = []int32{
|
||||
5, // 0: permission.v1.GetRolePermissionTreeRequest.active_only:type_name -> google.protobuf.BoolValue
|
||||
2, // 1: permission.v1.RolePermissionTreeResponse.data:type_name -> permission.v1.RolePermissionData
|
||||
3, // 2: permission.v1.RolePermissionData.access:type_name -> permission.v1.AccessTreeItem
|
||||
4, // 3: permission.v1.AccessTreeItem.permission:type_name -> permission.v1.Permission
|
||||
3, // 4: permission.v1.AccessTreeItem.children:type_name -> permission.v1.AccessTreeItem
|
||||
0, // 5: permission.v1.RolPermissionService.GetRolePermissionTree:input_type -> permission.v1.GetRolePermissionTreeRequest
|
||||
1, // 6: permission.v1.RolPermissionService.GetRolePermissionTree:output_type -> permission.v1.RolePermissionTreeResponse
|
||||
6, // [6:7] is the sub-list for method output_type
|
||||
5, // [5:6] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_init() }
|
||||
func file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_init() {
|
||||
if File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto != nil {
|
||||
return
|
||||
}
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc), len(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes,
|
||||
DependencyIndexes: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs,
|
||||
MessageInfos: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes,
|
||||
}.Build()
|
||||
File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto = out.File
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes = nil
|
||||
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v3.12.4
|
||||
// source: internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto
|
||||
|
||||
package permissionV1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
RolPermissionService_GetRolePermissionTree_FullMethodName = "/permission.v1.RolPermissionService/GetRolePermissionTree"
|
||||
)
|
||||
|
||||
// RolPermissionServiceClient is the client API for RolPermissionService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// Definisi service.
|
||||
type RolPermissionServiceClient interface {
|
||||
// Mengambil permission tree untuk role dan/atau group tertentu.
|
||||
GetRolePermissionTree(ctx context.Context, in *GetRolePermissionTreeRequest, opts ...grpc.CallOption) (*RolePermissionTreeResponse, error)
|
||||
}
|
||||
|
||||
type rolPermissionServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewRolPermissionServiceClient(cc grpc.ClientConnInterface) RolPermissionServiceClient {
|
||||
return &rolPermissionServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *rolPermissionServiceClient) GetRolePermissionTree(ctx context.Context, in *GetRolePermissionTreeRequest, opts ...grpc.CallOption) (*RolePermissionTreeResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RolePermissionTreeResponse)
|
||||
err := c.cc.Invoke(ctx, RolPermissionService_GetRolePermissionTree_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RolPermissionServiceServer is the server API for RolPermissionService service.
|
||||
// All implementations must embed UnimplementedRolPermissionServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// Definisi service.
|
||||
type RolPermissionServiceServer interface {
|
||||
// Mengambil permission tree untuk role dan/atau group tertentu.
|
||||
GetRolePermissionTree(context.Context, *GetRolePermissionTreeRequest) (*RolePermissionTreeResponse, error)
|
||||
mustEmbedUnimplementedRolPermissionServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedRolPermissionServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedRolPermissionServiceServer struct{}
|
||||
|
||||
func (UnimplementedRolPermissionServiceServer) GetRolePermissionTree(context.Context, *GetRolePermissionTreeRequest) (*RolePermissionTreeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRolePermissionTree not implemented")
|
||||
}
|
||||
func (UnimplementedRolPermissionServiceServer) mustEmbedUnimplementedRolPermissionServiceServer() {}
|
||||
func (UnimplementedRolPermissionServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeRolPermissionServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to RolPermissionServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeRolPermissionServiceServer interface {
|
||||
mustEmbedUnimplementedRolPermissionServiceServer()
|
||||
}
|
||||
|
||||
func RegisterRolPermissionServiceServer(s grpc.ServiceRegistrar, srv RolPermissionServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedRolPermissionServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&RolPermissionService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _RolPermissionService_GetRolePermissionTree_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRolePermissionTreeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RolPermissionServiceServer).GetRolePermissionTree(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RolPermissionService_GetRolePermissionTree_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RolPermissionServiceServer).GetRolePermissionTree(ctx, req.(*GetRolePermissionTreeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// RolPermissionService_ServiceDesc is the grpc.ServiceDesc for RolPermissionService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var RolPermissionService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "permission.v1.RolPermissionService",
|
||||
HandlerType: (*RolPermissionServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetRolePermissionTree",
|
||||
Handler: _RolPermissionService_GetRolePermissionTree_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto",
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"service/pkg/errors"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// / handleServiceError mengkonversi error dari service layer ke gRPC status error
|
||||
func handleServiceError(err error) error {
|
||||
appErr := errors.FromError(err)
|
||||
if appErr == nil {
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
|
||||
// Mapping kategori error ke gRPC codes
|
||||
switch appErr.Category() {
|
||||
case errors.CategoryValidation:
|
||||
return status.Error(codes.InvalidArgument, appErr.Error())
|
||||
case errors.CategoryNotFound:
|
||||
return status.Error(codes.NotFound, appErr.Error())
|
||||
case errors.CategoryConflict:
|
||||
return status.Error(codes.AlreadyExists, appErr.Error())
|
||||
case errors.CategoryUnauthorized:
|
||||
return status.Error(codes.Unauthenticated, appErr.Error())
|
||||
case errors.CategoryForbidden:
|
||||
return status.Error(codes.PermissionDenied, appErr.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, appErr.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/transport/grpc/mappers"
|
||||
"service/internal/master/role/permission"
|
||||
|
||||
// Asumsi path ke proto-generated file
|
||||
permissionV1 "service/internal/infrastructure/transport/grpc/gen/permission/v1"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// PermissionHandler mengimplementasikan gRPC service untuk role permission.
|
||||
type PermissionHandler struct {
|
||||
permissionV1.UnimplementedRolPermissionServiceServer
|
||||
service permission.Service
|
||||
}
|
||||
|
||||
// NewPermissionHandler membuat instance baru dari PermissionHandler.
|
||||
func NewPermissionHandler(service permission.Service) *PermissionHandler {
|
||||
return &PermissionHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRolePermissionTree adalah implementasi dari RPC.
|
||||
func (h *PermissionHandler) GetRolePermissionTree(ctx context.Context, req *permissionV1.GetRolePermissionTreeRequest) (*permissionV1.RolePermissionTreeResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "request cannot be nil")
|
||||
}
|
||||
|
||||
// Konversi wrapper BoolValue ke pointer *bool yang diharapkan oleh service
|
||||
var activeOnly *bool
|
||||
if val := req.GetActiveOnly(); val != nil {
|
||||
b := val.GetValue()
|
||||
activeOnly = &b
|
||||
}
|
||||
|
||||
tree, err := h.service.GetRolePermissionTree(ctx, req.GetRoleKeycloak(), req.GetGroupKeycloak(), activeOnly)
|
||||
if err != nil {
|
||||
return nil, handleServiceError(err)
|
||||
}
|
||||
|
||||
// Menggunakan mapper untuk mengubah hasil dari service ke format proto
|
||||
protoResponse := mappers.MapRolePermissionTreeToProto(tree)
|
||||
|
||||
return protoResponse, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package mappers
|
||||
|
||||
import "fmt"
|
||||
|
||||
func convertToString(value interface{}) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int, int64, float64:
|
||||
return fmt.Sprintf("%v", v)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
permissionV1 "service/internal/infrastructure/transport/grpc/gen/permission/v1"
|
||||
"service/internal/master/role/permission"
|
||||
)
|
||||
|
||||
// mapAccessTreeToProto secara rekursif mengubah tree dari service ke format proto.
|
||||
func mapAccessTreeToProto(serviceAccess []*permission.AccessTreeItem) []*permissionV1.AccessTreeItem {
|
||||
if serviceAccess == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
protoAccess := make([]*permissionV1.AccessTreeItem, len(serviceAccess))
|
||||
for i, item := range serviceAccess {
|
||||
protoItem := &permissionV1.AccessTreeItem{
|
||||
Id: item.ID,
|
||||
Name: item.Name,
|
||||
Icon: item.Icon,
|
||||
Url: item.URL,
|
||||
Level: int32(item.Level),
|
||||
Sort: int32(item.Sort),
|
||||
Active: item.Active,
|
||||
Children: mapAccessTreeToProto(item.Children), // Panggilan rekursif
|
||||
}
|
||||
|
||||
if item.Permission != nil {
|
||||
protoItem.Permission = &permissionV1.Permission{
|
||||
Create: item.Permission.Create,
|
||||
Read: item.Permission.Read,
|
||||
Update: item.Permission.Update,
|
||||
Delete: item.Permission.Delete,
|
||||
Disable: item.Permission.Disable,
|
||||
}
|
||||
}
|
||||
protoAccess[i] = protoItem
|
||||
}
|
||||
return protoAccess
|
||||
}
|
||||
|
||||
// MapRolePermissionTreeToProto adalah fungsi utama untuk mengubah seluruh respons tree.
|
||||
func MapRolePermissionTreeToProto(serviceResponse *permission.RolePermissionTreeResponse) *permissionV1.RolePermissionTreeResponse {
|
||||
if serviceResponse == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &permissionV1.RolePermissionTreeResponse{
|
||||
Success: serviceResponse.Success,
|
||||
Data: &permissionV1.RolePermissionData{
|
||||
Role: serviceResponse.Data.Role,
|
||||
// Group: serviceResponse.Data.Group,
|
||||
Access: mapAccessTreeToProto(serviceResponse.Data.Access),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package master.v1;
|
||||
|
||||
// Path should be relative to the module root.
|
||||
// The package name is specified after the semicolon.
|
||||
option go_package = "internal/infrastructure/transport/grpc/gen/master/role/master/v1;masterV1";
|
||||
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// Service definition for RoleAccessRolMaster.
|
||||
service RoleAccessRolMasterService {
|
||||
// Get a single RoleAccessRolMaster by its ID.
|
||||
rpc GetRoleAccessRolMaster(GetRoleAccessRolMasterRequest) returns (RoleAccessRolMasterResponse);
|
||||
|
||||
// Get a list of RoleAccessRolMasters with pagination and filtering.
|
||||
rpc ListRoleAccessRolMasters(ListRoleAccessRolMastersRequest) returns (ListRoleAccessRolMastersResponse);
|
||||
|
||||
// Create a new RoleAccessRolMaster.
|
||||
rpc CreateRoleAccessRolMaster(CreateRoleAccessRolMasterRequest) returns (RoleAccessRolMasterResponse);
|
||||
|
||||
// Update an existing RoleAccessRolMaster.
|
||||
rpc UpdateRoleAccessRolMaster(UpdateRoleAccessRolMasterRequest) returns (RoleAccessRolMasterResponse);
|
||||
|
||||
// Delete a RoleAccessRolMaster by its ID.
|
||||
rpc DeleteRoleAccessRolMaster(DeleteRoleAccessRolMasterRequest) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
// The main message representing a RoleAccessRolMaster.
|
||||
message RoleAccessRolMaster {
|
||||
string id = 1;
|
||||
optional string name = 2;
|
||||
optional bool active = 3;
|
||||
optional google.protobuf.Timestamp created_at = 4;
|
||||
optional google.protobuf.Timestamp updated_at = 5;
|
||||
optional string s_e_l_e_c_t = 6;
|
||||
}
|
||||
|
||||
// --- Request/Response Messages ---
|
||||
|
||||
message GetRoleAccessRolMasterRequest {
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
message RoleAccessRolMasterResponse {
|
||||
RoleAccessRolMaster data = 1;
|
||||
}
|
||||
|
||||
message ListRoleAccessRolMastersRequest {
|
||||
int32 page = 1;
|
||||
int32 page_size = 2;
|
||||
// Add filter fields here if needed
|
||||
}
|
||||
|
||||
message ListRoleAccessRolMastersResponse {
|
||||
repeated RoleAccessRolMaster data = 1;
|
||||
int64 total_items = 2;
|
||||
}
|
||||
|
||||
message CreateRoleAccessRolMasterRequest {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
bool active = 3;
|
||||
google.protobuf.Timestamp created_at = 4;
|
||||
google.protobuf.Timestamp updated_at = 5;
|
||||
string s_e_l_e_c_t = 6;
|
||||
}
|
||||
|
||||
message UpdateRoleAccessRolMasterRequest {
|
||||
int64 id = 1;
|
||||
optional string id = 2;
|
||||
optional string name = 3;
|
||||
optional bool active = 4;
|
||||
optional google.protobuf.Timestamp created_at = 5;
|
||||
optional google.protobuf.Timestamp updated_at = 6;
|
||||
optional string s_e_l_e_c_t = 7;
|
||||
}
|
||||
|
||||
message DeleteRoleAccessRolMasterRequest {
|
||||
int64 id = 1;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package permission.v1;
|
||||
|
||||
// Opsi go_package ini sangat penting.
|
||||
// Path harus relatif terhadap root modul (tanpa nama modul "service-general").
|
||||
// Nama paket eksplisit (permissionV1) ditambahkan setelah titik koma.
|
||||
option go_package = "internal/infrastructure/transport/grpc/gen/permission/v1;permissionV1";
|
||||
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
// Definisi service.
|
||||
service RolPermissionService {
|
||||
// Mengambil permission tree untuk role dan/atau group tertentu.
|
||||
rpc GetRolePermissionTree(GetRolePermissionTreeRequest) returns (RolePermissionTreeResponse);
|
||||
}
|
||||
|
||||
// Pesan request untuk RPC GetRolePermissionTree.
|
||||
message GetRolePermissionTreeRequest {
|
||||
string role_keycloak = 1;
|
||||
repeated string group_keycloak = 2;
|
||||
// Menggunakan wrapper BoolValue untuk membedakan antara 'false' dan 'tidak diset'.
|
||||
google.protobuf.BoolValue active_only = 3;
|
||||
}
|
||||
|
||||
// Pesan response untuk RPC GetRolePermissionTree.
|
||||
message RolePermissionTreeResponse {
|
||||
bool success = 1;
|
||||
RolePermissionData data = 2;
|
||||
}
|
||||
|
||||
message RolePermissionData {
|
||||
string role = 1;
|
||||
repeated string group = 2;
|
||||
repeated AccessTreeItem access = 3;
|
||||
}
|
||||
|
||||
message AccessTreeItem {
|
||||
int64 id = 1;
|
||||
string name = 2;
|
||||
string icon = 3;
|
||||
string url = 4;
|
||||
int32 level = 5;
|
||||
int32 sort = 6;
|
||||
bool active = 7;
|
||||
optional Permission permission = 8;
|
||||
repeated AccessTreeItem children = 9;
|
||||
}
|
||||
|
||||
message Permission {
|
||||
bool create = 1;
|
||||
bool read = 2;
|
||||
bool update = 3;
|
||||
bool delete = 4;
|
||||
bool disable = 5;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package auth;
|
||||
|
||||
// PERBAIKAN: Tambahkan 'transport' dan hapus suffix '/auth' agar sesuai direktori import
|
||||
option go_package = "gen/auth/v1;auth";
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
string token = 1;
|
||||
string expires_at = 2;
|
||||
User user = 3;
|
||||
}
|
||||
|
||||
message RegisterRequest {
|
||||
string username = 1;
|
||||
string email = 2;
|
||||
string password = 3;
|
||||
string full_name = 4;
|
||||
string role = 5;
|
||||
}
|
||||
|
||||
message RegisterResponse {
|
||||
string id = 1;
|
||||
string username = 2;
|
||||
string email = 3;
|
||||
string full_name = 4;
|
||||
}
|
||||
|
||||
message ValidateTokenRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message ValidateTokenResponse {
|
||||
bool valid = 1;
|
||||
User user = 2;
|
||||
}
|
||||
|
||||
message LogoutRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message LogoutResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message User {
|
||||
string id = 1;
|
||||
string username = 2;
|
||||
string email = 3;
|
||||
string full_name = 4;
|
||||
string role = 5;
|
||||
}
|
||||
|
||||
service AuthService {
|
||||
rpc Register (RegisterRequest) returns (RegisterResponse);
|
||||
rpc Login(LoginRequest) returns (LoginResponse);
|
||||
rpc ValidateToken (ValidateTokenRequest) returns (ValidateTokenResponse);
|
||||
rpc Logout (LogoutRequest) returns (LogoutResponse);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/internal/infrastructure/transport/grpc/handlers"
|
||||
"service/pkg/logger"
|
||||
|
||||
// Import generated proto files. Asumsi path ini benar.
|
||||
permissionV1 "service/internal/infrastructure/transport/grpc/gen/permission/v1"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
type ServiceRegistry struct {
|
||||
PermissionHandler *handlers.PermissionHandler
|
||||
}
|
||||
|
||||
// GRPCServer membungkus instance grpc.Server.
|
||||
type GRPCServer struct {
|
||||
server *grpc.Server
|
||||
config *config.ServerGRPCConfig
|
||||
registry *ServiceRegistry
|
||||
}
|
||||
|
||||
// NewGRPCServer membuat instance server gRPC baru dan mendaftarkan semua service dari registry.
|
||||
func NewGRPCServer(config *config.ServerGRPCConfig, registry *ServiceRegistry) *GRPCServer {
|
||||
srv := grpc.NewServer()
|
||||
|
||||
// Daftarkan semua service yang ada di registry
|
||||
if registry.PermissionHandler != nil {
|
||||
permissionV1.RegisterRolPermissionServiceServer(srv, registry.PermissionHandler)
|
||||
logger.Default().Info("Registered RolPermission gRPC service")
|
||||
}
|
||||
|
||||
// Aktifkan reflection agar bisa di-debug dengan tools seperti grpcurl
|
||||
reflection.Register(srv)
|
||||
|
||||
return &GRPCServer{server: srv, config: config, registry: registry}
|
||||
}
|
||||
|
||||
// Start menjalankan gRPC server.
|
||||
func (s *GRPCServer) Start() error {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on port %d: %w", s.config.Port, err)
|
||||
}
|
||||
return s.server.Serve(lis)
|
||||
}
|
||||
|
||||
// Stop menghentikan gRPC server secara graceful.
|
||||
func (s *GRPCServer) Stop() {
|
||||
s.server.GracefulStop()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package antrol
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
antrolService "service/internal/bpjs/antrol/reference"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AntrolHandler struct {
|
||||
service antrolService.Service
|
||||
}
|
||||
|
||||
func NewAntrolHandler(service antrolService.Service) *AntrolHandler {
|
||||
return &AntrolHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *AntrolHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/bpjs/antrol/reference")
|
||||
{
|
||||
group.GET("/poli", h.GetRefPoli)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AntrolHandler) GetRefPoli(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
res, err := h.service.GetRefPoli(ctx)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Antrol Poli reference", res)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package aplicare
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
bedService "service/internal/bpjs/aplicare/bed"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BedHandler struct {
|
||||
service bedService.Service
|
||||
}
|
||||
|
||||
func NewBedHandler(service bedService.Service) *BedHandler {
|
||||
return &BedHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *BedHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/bpjs/aplicare/bed")
|
||||
{
|
||||
group.GET("/:kdppk/:start/:limit", h.GetBedList)
|
||||
group.POST("/:kdppk", h.CreateBed)
|
||||
group.PUT("/:kdppk", h.UpdateBed)
|
||||
group.DELETE("/:kdppk", h.DeleteBed)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BedHandler) GetBedList(c *gin.Context) {
|
||||
kdPpk := c.Param("kdppk")
|
||||
start, _ := strconv.Atoi(c.Param("start"))
|
||||
limit, _ := strconv.Atoi(c.Param("limit"))
|
||||
|
||||
ctx := c.Request.Context()
|
||||
res, err := h.service.GetBedList(ctx, kdPpk, start, limit)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Aplicares bed list", res)
|
||||
}
|
||||
|
||||
func (h *BedHandler) CreateBed(c *gin.Context) {
|
||||
kdPpk := c.Param("kdppk")
|
||||
var req bedService.BedData
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.CreateBed(ctx, kdPpk, req); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created BPJS Aplicares bed data", nil)
|
||||
}
|
||||
|
||||
func (h *BedHandler) UpdateBed(c *gin.Context) {
|
||||
kdPpk := c.Param("kdppk")
|
||||
var req bedService.BedData
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.UpdateBed(ctx, kdPpk, req); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully updated BPJS Aplicares bed data", nil)
|
||||
}
|
||||
|
||||
func (h *BedHandler) DeleteBed(c *gin.Context) {
|
||||
kdPpk := c.Param("kdppk")
|
||||
var req bedService.BedDeletePayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.DeleteBed(ctx, kdPpk, req); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully deleted BPJS Aplicares bed data", nil)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package apotek
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
dphoService "service/internal/bpjs/apotek/reference/dpho"
|
||||
poliService "service/internal/bpjs/apotek/reference/poli"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ReferenceHandler struct {
|
||||
dphoService dphoService.Service
|
||||
poliService poliService.Service
|
||||
}
|
||||
|
||||
func NewReferenceHandler(dpho dphoService.Service, poli poliService.Service) *ReferenceHandler {
|
||||
return &ReferenceHandler{
|
||||
dphoService: dpho,
|
||||
poliService: poli,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ReferenceHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/bpjs/apotek/reference")
|
||||
{
|
||||
group.GET("/dpho", h.GetDPHO)
|
||||
group.GET("/poli/:param", h.GetPoli)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ReferenceHandler) GetDPHO(c *gin.Context) {
|
||||
res, err := h.dphoService.GetDPHO(c.Request.Context())
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Apotek DPHO reference", res)
|
||||
}
|
||||
|
||||
func (h *ReferenceHandler) GetPoli(c *gin.Context) {
|
||||
param := c.Param("param")
|
||||
res, err := h.poliService.GetPoli(c.Request.Context(), param)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved BPJS Apotek Poli reference", res)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package vclaim
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
pesertaService "service/internal/bpjs/vclaim/peserta"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PesertaHandler struct {
|
||||
service pesertaService.Service
|
||||
}
|
||||
|
||||
func NewPesertaHandler(service pesertaService.Service) *PesertaHandler {
|
||||
return &PesertaHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *PesertaHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/bpjs/vclaim/peserta")
|
||||
{
|
||||
group.GET("/nik/:nik", h.GetPesertaByNIK)
|
||||
group.GET("/nokartu/:nokartu", h.GetPesertaByNoKartu)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PesertaHandler) GetPesertaByNIK(c *gin.Context) {
|
||||
nik := c.Param("nik")
|
||||
tglSEP := c.Query("tgl_sep")
|
||||
if tglSEP == "" {
|
||||
tglSEP = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
res, err := h.service.GetPesertaByNIK(ctx, nik, tglSEP)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved participant data by NIK", res)
|
||||
}
|
||||
|
||||
func (h *PesertaHandler) GetPesertaByNoKartu(c *gin.Context) {
|
||||
noKartu := c.Param("nokartu")
|
||||
tglSEP := c.Query("tgl_sep")
|
||||
if tglSEP == "" {
|
||||
tglSEP = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
res, err := h.service.GetPesertaByNoKartu(ctx, noKartu, tglSEP)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved participant data by card number", res)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package vclaim
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
sepService "service/internal/bpjs/vclaim/sep"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SepHandler menangani semua request HTTP terkait VClaim.
|
||||
type SepHandler struct {
|
||||
sepService sepService.Service
|
||||
}
|
||||
|
||||
// NewSepHandler membuat instance SepHandler baru.
|
||||
func NewSepHandler(sepService sepService.Service) *SepHandler {
|
||||
return &SepHandler{
|
||||
sepService: sepService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan semua rute untuk BPJS VClaim.
|
||||
func (h *SepHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
bpjsGroup := router.Group("/bpjs/vclaim")
|
||||
{
|
||||
sepGroup := bpjsGroup.Group("/sep")
|
||||
{
|
||||
sepGroup.POST("", h.CreateSEP)
|
||||
sepGroup.PUT("", h.UpdateSEP)
|
||||
// BPJS API untuk delete menggunakan method POST, tapi kita ekspos sebagai DELETE untuk konsistensi RESTful.
|
||||
sepGroup.DELETE("", h.DeleteSEP)
|
||||
sepGroup.GET("/:nosep", h.GetSEPDetail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSEP menangani request untuk membuat SEP baru.
|
||||
func (h *SepHandler) CreateSEP(c *gin.Context) {
|
||||
var req sepService.CreateSEPRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.sepService.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created SEP", result)
|
||||
}
|
||||
|
||||
// UpdateSEP menangani request untuk memperbarui SEP.
|
||||
func (h *SepHandler) UpdateSEP(c *gin.Context) {
|
||||
var req sepService.UpdateSEPRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.sepService.Update(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully updated SEP", result)
|
||||
}
|
||||
|
||||
// GetSEPDetail menangani request untuk mendapatkan detail SEP.
|
||||
func (h *SepHandler) GetSEPDetail(c *gin.Context) {
|
||||
noSEP := c.Param("no_sep")
|
||||
|
||||
result, err := h.sepService.GetDetail(c.Request.Context(), noSEP)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved SEP detail", result)
|
||||
}
|
||||
|
||||
// DeleteSEP menangani request untuk menghapus SEP.
|
||||
func (h *SepHandler) DeleteSEP(c *gin.Context) {
|
||||
var req sepService.DeleteSEPRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body for SEP deletion", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.sepService.Delete(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully deleted SEP", result)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
authService "service/internal/auth"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
service authService.Service
|
||||
}
|
||||
|
||||
func NewAuthHandler(service authService.Service) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
auth := router.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", h.Login)
|
||||
auth.POST("/register", h.Register)
|
||||
auth.POST("/refresh", h.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterProtectedRoutes mendaftarkan endpoint auth yang membutuhkan token
|
||||
func (h *AuthHandler) RegisterProtectedRoutes(router *gin.RouterGroup) {
|
||||
auth := router.Group("/auth")
|
||||
{
|
||||
auth.POST("/logout", h.Logout)
|
||||
auth.GET("/info", h.TokenInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
//
|
||||
// @Summary Login user
|
||||
// @Description Authenticate user and return JWT & Refresh tokens
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body authService.LoginRequest true "Login credentials"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req authService.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Pemanggilan service secara by-value
|
||||
resp, err := h.service.Login(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Login successful", resp)
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
//
|
||||
// @Summary Register user
|
||||
// @Description Register a new user
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body authService.RegisterRequest true "Registration details"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req authService.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.Register(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "User registered successfully", resp)
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
//
|
||||
// @Summary Refresh Token
|
||||
// @Description Refresh expired access token using refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body authService.RefreshTokenRequest true "Refresh token payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
var req authService.RefreshTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request payload", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.service.RefreshToken(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Token refreshed successfully", resp)
|
||||
}
|
||||
|
||||
// Logout godoc
|
||||
//
|
||||
// @Summary Logout user
|
||||
// @Description Logout current user and blacklist the token
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /auth/logout [post]
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
response.Error(c, http.StatusBadRequest, "Token required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(token) > 7 && token[:7] == "Bearer " {
|
||||
token = token[7:]
|
||||
}
|
||||
|
||||
err := h.service.Logout(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Logged out successfully", nil)
|
||||
}
|
||||
|
||||
// TokenInfo godoc
|
||||
//
|
||||
// @Summary Get Token Info
|
||||
// @Description Retrieve information about the currently active token/user
|
||||
// @Tags auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /auth/info [get]
|
||||
func (h *AuthHandler) TokenInfo(c *gin.Context) {
|
||||
// Data ini otomatis di-set oleh provider.go (UnifiedAuthMiddleware)
|
||||
authProvider := c.GetString("auth_provider")
|
||||
userID := c.GetString("user_id")
|
||||
username := c.GetString("username")
|
||||
email := c.GetString("email")
|
||||
role := c.GetString("role")
|
||||
name := c.GetString("name")
|
||||
|
||||
data := gin.H{
|
||||
"auth_provider": authProvider, // Akan bernilai: "jwt", "keycloak", atau "static"
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"role": role,
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Current active token information", data)
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/internal/interfaces/minio"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HealthHandler handles health check endpoints
|
||||
type HealthHandler struct {
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
config *config.Config
|
||||
cacheManager *cache.Manager // Alternatif untuk Redis client
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
// NewHealthHandlerWithCache creates a new health handler with cache manager
|
||||
func NewHealthHandlerWithCache(db *gorm.DB, cacheManager *cache.Manager, config *config.Config, dbManager database.Service) *HealthHandler {
|
||||
handler := &HealthHandler{
|
||||
db: db,
|
||||
config: config,
|
||||
cacheManager: cacheManager,
|
||||
dbManager: dbManager,
|
||||
}
|
||||
|
||||
// Coba dapatkan Redis client dari cache manager
|
||||
if cacheManager != nil {
|
||||
if redisClientInterface := cacheManager.GetRedisClient(); redisClientInterface != nil {
|
||||
if client, ok := redisClientInterface.(*redis.Client); ok {
|
||||
handler.redisClient = client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// HealthCheckComplete performs comprehensive health check
|
||||
func (h *HealthHandler) HealthCheckComplete(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
health := gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC(),
|
||||
"uptime": time.Since(startTime).Milliseconds(),
|
||||
"version": "1.0.0",
|
||||
"service": "service",
|
||||
}
|
||||
|
||||
// Check database
|
||||
dbStatus := h.checkDatabase()
|
||||
health["database"] = dbStatus
|
||||
|
||||
// Check cache
|
||||
cacheStatus := h.checkCache()
|
||||
health["cache"] = cacheStatus
|
||||
|
||||
// Check external services
|
||||
externalStatus := h.checkExternalServices()
|
||||
health["external_services"] = externalStatus
|
||||
|
||||
// Check Minio
|
||||
minioStatus := h.checkMinio()
|
||||
health["minio"] = minioStatus
|
||||
|
||||
// Determine overall status
|
||||
if dbStatus["status"] != "UP" || cacheStatus["status"] != "UP" || (minioStatus["status"] != "UP" && minioStatus["status"] != "DISABLED") {
|
||||
health["status"] = "DOWN"
|
||||
c.JSON(http.StatusServiceUnavailable, health)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, health)
|
||||
}
|
||||
|
||||
// HealthCheckDatabase checks database connectivity
|
||||
func (h *HealthHandler) HealthCheckDatabase(c *gin.Context) {
|
||||
status := h.checkDatabase()
|
||||
|
||||
if status["status"] != "UP" {
|
||||
c.JSON(http.StatusServiceUnavailable, status)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// HealthCheckCache checks cache connectivity
|
||||
func (h *HealthHandler) HealthCheckCache(c *gin.Context) {
|
||||
status := h.checkCache()
|
||||
|
||||
if status["status"] != "UP" {
|
||||
c.JSON(http.StatusServiceUnavailable, status)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// HealthCheckExternal checks external service connectivity
|
||||
func (h *HealthHandler) HealthCheckExternal(c *gin.Context) {
|
||||
status := h.checkExternalServices()
|
||||
|
||||
// Hanya return 503 (Unavailable) jika status benar-benar DOWN, bukan saat sekadar DEGRADED.
|
||||
if status["status"] == "DOWN" {
|
||||
c.JSON(http.StatusServiceUnavailable, status)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// HealthCheckMinio checks Minio Object Storage connectivity
|
||||
func (h *HealthHandler) HealthCheckMinio(c *gin.Context) {
|
||||
status := h.checkMinio()
|
||||
|
||||
if status["status"] == "DOWN" {
|
||||
c.JSON(http.StatusServiceUnavailable, status)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// TestUploadMinio is a testing endpoint to upload a file to Minio
|
||||
func (h *HealthHandler) TestUploadMinio(c *gin.Context) {
|
||||
if minio.I == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Minio client is not initialized or disconnected"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File is required (form-data key: 'file')"})
|
||||
return
|
||||
}
|
||||
|
||||
// Gunakan bucket dari request body (jika ada), atau default ke config/nama statis
|
||||
bucketName := c.DefaultPostForm("bucket", "dev-test")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Ensure bucket exists
|
||||
exists, err := minio.I.BucketExists(ctx, bucketName)
|
||||
if err == nil && !exists {
|
||||
err = minio.I.MakeBucket(ctx, bucketName, miniogo.MakeBucketOptions{Region: h.config.Minio.Region})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check bucket status: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
objectName := fmt.Sprintf("%d-%s", time.Now().Unix(), file.Filename)
|
||||
info, err := minio.I.PutObject(ctx, bucketName, objectName, src, file.Size, miniogo.PutObjectOptions{
|
||||
ContentType: file.Header.Get("Content-Type"),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload to Minio: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "File uploaded successfully",
|
||||
"data": info,
|
||||
})
|
||||
}
|
||||
|
||||
// checkDatabase checks database health
|
||||
func (h *HealthHandler) checkDatabase() gin.H {
|
||||
if h.dbManager == nil {
|
||||
return gin.H{
|
||||
"status": "UNKNOWN",
|
||||
"error": "Database manager not initialized",
|
||||
}
|
||||
}
|
||||
|
||||
allDbInfo := h.dbManager.GetAllDatabasesInfo()
|
||||
overallStatus := "UP"
|
||||
|
||||
// Iterate through the map and check each database
|
||||
for name, info := range allDbInfo {
|
||||
dbInfo, ok := info.(gin.H)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Standardize the status to "UP" if it's "connected" or "healthy"
|
||||
if status, ok := dbInfo["status"].(string); ok {
|
||||
if status == "connected" || status == "healthy" || status == "UP" {
|
||||
dbInfo["status"] = "UP"
|
||||
} else {
|
||||
overallStatus = "DOWN"
|
||||
}
|
||||
}
|
||||
allDbInfo[name] = dbInfo // Update the map with the checked info
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"status": overallStatus,
|
||||
"components": allDbInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// checkCache checks Redis/cache health
|
||||
func (h *HealthHandler) checkCache() gin.H {
|
||||
if h.cacheManager == nil {
|
||||
return gin.H{"status": "UNKNOWN", "error": "Cache manager not initialized"}
|
||||
}
|
||||
|
||||
if !h.config.Cache.Enabled {
|
||||
return gin.H{"status": "UP", "details": gin.H{"provider": "noop", "message": "Cache is disabled"}}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
err := h.cacheManager.Health(ctx)
|
||||
latency := time.Since(start)
|
||||
|
||||
details := gin.H{
|
||||
"provider": "redis",
|
||||
"latency": latency.String(),
|
||||
"latency_ms": latency.Milliseconds(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
details["error"] = err.Error()
|
||||
return gin.H{"status": "DOWN", "details": details}
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"status": "UP",
|
||||
"details": details,
|
||||
}
|
||||
}
|
||||
|
||||
// checkMinio checks Minio Object Storage health
|
||||
func (h *HealthHandler) checkMinio() gin.H {
|
||||
if h.config == nil || h.config.Minio.Endpoint == "" {
|
||||
return gin.H{"status": "DISABLED", "message": "Minio is not configured in .env"}
|
||||
}
|
||||
|
||||
if minio.I == nil {
|
||||
return gin.H{"status": "DOWN", "message": "Minio global client is nil"}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
// Panggilan ringan API minio untuk memastikan server hidup & kredensial benar
|
||||
_, err := minio.I.ListBuckets(ctx)
|
||||
latency := time.Since(start)
|
||||
|
||||
details := gin.H{
|
||||
"endpoint": h.config.Minio.Endpoint,
|
||||
"latency_ms": latency.Milliseconds(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
details["error"] = err.Error()
|
||||
return gin.H{"status": "DOWN", "details": details}
|
||||
}
|
||||
|
||||
return gin.H{"status": "UP", "details": details}
|
||||
}
|
||||
|
||||
// checkExternalServices checks external service health
|
||||
func (h *HealthHandler) checkExternalServices() gin.H {
|
||||
status := gin.H{
|
||||
"status": "UP",
|
||||
"message": "All configured external services are reachable",
|
||||
}
|
||||
|
||||
hasDegraded := false
|
||||
hasDown := false
|
||||
downServices := []string{}
|
||||
degradedServices := []string{}
|
||||
activeServices := 0
|
||||
|
||||
// Check BPJS service if configured
|
||||
if h.config != nil {
|
||||
bpjsStatus := h.checkBPJSService()
|
||||
status["bpjs"] = bpjsStatus
|
||||
|
||||
if s, ok := bpjsStatus["status"].(string); ok && s != "DISABLED" {
|
||||
activeServices++
|
||||
if s == "DOWN" {
|
||||
hasDown = true
|
||||
downServices = append(downServices, "BPJS")
|
||||
} else if s == "DEGRADED" {
|
||||
hasDegraded = true
|
||||
degradedServices = append(degradedServices, "BPJS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check SatuSehat service if configured
|
||||
if h.config != nil {
|
||||
satuSehatStatus := h.checkSatuSehatService()
|
||||
status["satu_sehat"] = satuSehatStatus
|
||||
|
||||
if s, ok := satuSehatStatus["status"].(string); ok && s != "DISABLED" {
|
||||
activeServices++
|
||||
if s == "DOWN" {
|
||||
hasDown = true
|
||||
downServices = append(downServices, "SatuSehat")
|
||||
} else if s == "DEGRADED" {
|
||||
hasDegraded = true
|
||||
degradedServices = append(degradedServices, "SatuSehat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if activeServices == 0 {
|
||||
status["status"] = "UP"
|
||||
status["message"] = "All external services are disabled"
|
||||
} else if hasDown {
|
||||
status["status"] = "DOWN"
|
||||
status["message"] = fmt.Sprintf("Some external services are unreachable: %v", downServices)
|
||||
} else if hasDegraded {
|
||||
status["status"] = "DEGRADED"
|
||||
status["message"] = fmt.Sprintf("Some external services are degraded: %v", degradedServices)
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// checkBPJSService checks BPJS VClaim service
|
||||
func (h *HealthHandler) checkBPJSService() gin.H {
|
||||
if !h.config.Bpjs.Enabled {
|
||||
return gin.H{
|
||||
"status": "DISABLED",
|
||||
"message": "BPJS integration is disabled in configuration",
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", h.config.Bpjs.BaseURL, nil)
|
||||
if err != nil {
|
||||
return gin.H{
|
||||
"status": "DOWN",
|
||||
"message": "Failed to create BPJS request: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return gin.H{
|
||||
"status": "DOWN",
|
||||
"message": "BPJS service unreachable: " + err.Error(),
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
status := "UP"
|
||||
message := "BPJS service is reachable"
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
message = "BPJS service is reachable and responding normally"
|
||||
case http.StatusUnauthorized:
|
||||
message = "BPJS service is reachable (Authentication required/Invalid Signature)"
|
||||
case http.StatusForbidden:
|
||||
status = "DEGRADED"
|
||||
message = "BPJS service is reachable, but access is forbidden (Check IP Whitelisting or Credentials)"
|
||||
case http.StatusNotFound:
|
||||
message = "BPJS service is reachable (Base URL responded with 404, server is UP)"
|
||||
default:
|
||||
if resp.StatusCode >= 500 {
|
||||
status = "DOWN"
|
||||
message = fmt.Sprintf("BPJS service returned server error: %s", resp.Status)
|
||||
} else {
|
||||
message = fmt.Sprintf("BPJS service is reachable (Status: %s)", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"status": status,
|
||||
"message": message,
|
||||
"status_code": resp.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// checkSatuSehatService checks SatuSehat FHIR service
|
||||
func (h *HealthHandler) checkSatuSehatService() gin.H {
|
||||
if !h.config.SatuSehat.Enabled {
|
||||
return gin.H{
|
||||
"status": "DISABLED",
|
||||
"message": "SatuSehat integration is disabled in configuration",
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 2 * time.Second, // Dipercepat agar health check tidak blocking terlalu lama
|
||||
}
|
||||
|
||||
checkURL := func(url string) (string, string, int) {
|
||||
if url == "" {
|
||||
return "DISABLED", "URL not configured", 0
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "DOWN", "Request creation failed: " + err.Error(), 0
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "DOWN", "Unreachable: " + err.Error(), 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return "UP", fmt.Sprintf("Reachable (Status: %s)", resp.Status), resp.StatusCode
|
||||
}
|
||||
|
||||
authStatus, authMsg, authCode := checkURL(h.config.SatuSehat.AuthURL)
|
||||
baseStatus, baseMsg, baseCode := checkURL(h.config.SatuSehat.BaseURL)
|
||||
consentStatus, consentMsg, _ := checkURL(h.config.SatuSehat.ConsentURL)
|
||||
kfaStatus, kfaMsg, _ := checkURL(h.config.SatuSehat.KFAURL)
|
||||
|
||||
overallStatus := "UP"
|
||||
overallMessage := "SatuSehat services are reachable"
|
||||
|
||||
// Evaluasi status keseluruhan berdasarkan Base URL & Auth URL
|
||||
if baseStatus == "DOWN" || authStatus == "DOWN" {
|
||||
overallStatus = "DOWN"
|
||||
overallMessage = "One or more critical SatuSehat services are unreachable"
|
||||
} else if baseCode == http.StatusForbidden || authCode == http.StatusForbidden {
|
||||
overallStatus = "DEGRADED"
|
||||
overallMessage = "Services are reachable, but access is forbidden (Check IP/Permissions)"
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"status": overallStatus,
|
||||
"message": overallMessage,
|
||||
"details": gin.H{
|
||||
"org_id": h.config.SatuSehat.OrgID,
|
||||
"fasyankes_id": h.config.SatuSehat.FasyakesID,
|
||||
"endpoints": gin.H{
|
||||
"auth": gin.H{"url": h.config.SatuSehat.AuthURL, "status": authStatus, "message": authMsg, "status_code": authCode},
|
||||
"fhir_base": gin.H{"url": h.config.SatuSehat.BaseURL, "status": baseStatus, "message": baseMsg, "status_code": baseCode},
|
||||
"consent": gin.H{"url": h.config.SatuSehat.ConsentURL, "status": consentStatus, "message": consentMsg},
|
||||
"kfa": gin.H{"url": h.config.SatuSehat.KFAURL, "status": kfaStatus, "message": kfaMsg},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
masterService "service/internal/master/role/master"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/logger"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RoleMasterHandler struct {
|
||||
service masterService.Service
|
||||
}
|
||||
|
||||
func NewRoleMasterHandler(service masterService.Service) *RoleMasterHandler {
|
||||
return &RoleMasterHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *RoleMasterHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/roles/master")
|
||||
{
|
||||
group.GET("", h.GetList)
|
||||
group.GET("/search", h.Search)
|
||||
group.GET("/:id", h.GetDetail)
|
||||
group.POST("", h.Create)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.DELETE("/:id", h.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// GetList godoc
|
||||
//
|
||||
// @Summary Get list of Role
|
||||
// @Description Retrieve a paginated list of RoleMaster
|
||||
// @Tags roles
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of items per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master [get]
|
||||
func (h *RoleMasterHandler) GetList(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
limit, _ = strconv.Atoi(limitStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
|
||||
offset = (offset - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
activeParam := c.Query("active")
|
||||
var activeFilter *bool // default nil (no filter / ambil semua)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeFilter = &isActive
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages list", data, meta)
|
||||
}
|
||||
|
||||
// GetDetail godoc
|
||||
//
|
||||
// @Summary Get RoleMaster detail
|
||||
// @Description Retrieve detailed information about a specific RoleMaster
|
||||
// @Tags roles
|
||||
// @Produce json
|
||||
// @Param id path int true "RoleMaster ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master/{id} [get]
|
||||
func (h *RoleMasterHandler) GetDetail(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetDetail(ctx, id)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved RoleMaster detail", result)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Search Role
|
||||
// @Description Search RoleMaster records using dynamic filters
|
||||
// @Tags roles
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master/search [get]
|
||||
func (h *RoleMasterHandler) Search(c *gin.Context) {
|
||||
// Parse Limit & Offset dengan fallback ke page & page_size
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
limit, _ = strconv.Atoi(limitStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
|
||||
offset = (offset - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil parameter filter
|
||||
// Ambil parameter filter secara dinamis
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
for key, values := range c.Request.URL.Query() {
|
||||
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
|
||||
continue
|
||||
}
|
||||
if len(values) > 0 && values[0] != "" {
|
||||
filters[key] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Default().WithContext(ctx).Info("Search request",
|
||||
logger.String("filters", fmt.Sprintf("%v", filters)),
|
||||
logger.String("sorts", fmt.Sprintf("%v", sorts)),
|
||||
logger.Int("offset", offset),
|
||||
logger.Int("limit", limit))
|
||||
|
||||
// Panggil service dengan parameter sort tambahan
|
||||
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
// Extract data dari map service untuk response format
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
// Hitung total pages
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{
|
||||
Page: page,
|
||||
Limit: limitVal,
|
||||
Total: int(total),
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RoleMaster search results", data, meta)
|
||||
}
|
||||
|
||||
// Create godoc
|
||||
//
|
||||
// @Summary Create new RoleMaster
|
||||
// @Description Create a new RoleMaster record
|
||||
// @Tags roles
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body masterService.RoleMasterRequest true "Payload"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master [post]
|
||||
func (h *RoleMasterHandler) Create(c *gin.Context) {
|
||||
var req masterService.RoleMasterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
created, err := h.service.Create(ctx, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created RoleMaster", created)
|
||||
}
|
||||
|
||||
// Update godoc
|
||||
//
|
||||
// @Summary Update an existing RoleMaster
|
||||
// @Description Update details of an existing RoleMaster record by ID
|
||||
// @Tags roles
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "RoleMaster ID"
|
||||
// @Param request body masterService.RoleMasterRequest true "Payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master/{id} [put]
|
||||
func (h *RoleMasterHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req masterService.RoleMasterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
updated, err := h.service.Update(ctx, id, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully updated RoleMaster", updated)
|
||||
}
|
||||
|
||||
// Delete godoc
|
||||
//
|
||||
// @Summary Delete a RoleMaster
|
||||
// @Description Delete a RoleMaster record by ID (soft delete)
|
||||
// @Tags roles
|
||||
// @Produce json
|
||||
// @Param id path int true "RoleMaster ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/master/{id} [delete]
|
||||
func (h *RoleMasterHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.Delete(ctx, id); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully deleted RoleMaster", nil)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
roleService "service/internal/master/role/pages"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/logger"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RolPagesHandler struct {
|
||||
service roleService.Service
|
||||
}
|
||||
|
||||
func NewRolPagesHandler(service roleService.Service) *RolPagesHandler {
|
||||
return &RolPagesHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *RolPagesHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/roles/pages")
|
||||
{
|
||||
group.GET("", h.GetList)
|
||||
group.GET("/tree", h.GetTree)
|
||||
group.GET("/tree/level/:level", h.GetTreeByLevel)
|
||||
group.GET("/search", h.Search)
|
||||
group.GET("/:id", h.GetDetail)
|
||||
group.POST("", h.Create)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.DELETE("/:id", h.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// GetList godoc
|
||||
//
|
||||
// @Summary Get list of Role Pages
|
||||
// @Description Retrieve a paginated list of Role Pages
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of items per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages [get]
|
||||
func (h *RolPagesHandler) GetList(c *gin.Context) {
|
||||
// Parse Limit & Offset dengan fallback ke page & page_size
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
limit, _ = strconv.Atoi(limitStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
|
||||
offset = (offset - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
activeParam := c.Query("active")
|
||||
var activeFilter *bool // default nil (no filter / ambil semua)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeFilter = &isActive
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages list", data, meta)
|
||||
}
|
||||
|
||||
// GetDetail godoc
|
||||
//
|
||||
// @Summary Get Role Pages detail
|
||||
// @Description Retrieve detailed information about a specific Role Pages
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Pages ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/{id} [get]
|
||||
func (h *RolPagesHandler) GetDetail(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetDetail(ctx, id)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved RolPages detail", result)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Search Role Pages
|
||||
// @Description Search Role Pages records using dynamic filters
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/search [get]
|
||||
func (h *RolPagesHandler) Search(c *gin.Context) {
|
||||
// Parse Limit & Offset dengan fallback ke page & page_size
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
limit, _ = strconv.Atoi(limitStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if offset, err := strconv.Atoi(offsetStr); err == nil && offset > 0 && limit > 0 {
|
||||
offset = (offset - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil parameter filter secara dinamis
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
for key, values := range c.Request.URL.Query() {
|
||||
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
|
||||
continue
|
||||
}
|
||||
if len(values) > 0 && values[0] != "" {
|
||||
filters[key] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Default().WithContext(ctx).Info("Search request",
|
||||
logger.String("filters", fmt.Sprintf("%v", filters)),
|
||||
logger.String("sorts", fmt.Sprintf("%v", sorts)),
|
||||
logger.Int("offset", offset),
|
||||
logger.Int("limit", limit))
|
||||
|
||||
// Panggil service dengan parameter sort tambahan
|
||||
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
// Extract data dari map service untuk response format
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{
|
||||
Page: page,
|
||||
Limit: limitVal,
|
||||
Total: int(total),
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPages search results", data, meta)
|
||||
}
|
||||
|
||||
// Create godoc
|
||||
//
|
||||
// @Summary Create new Role Pages
|
||||
// @Description Create a new Role Pages record
|
||||
// @Tags roles-pages
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body roleService.RolPagesRequest true "Payload"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages [post]
|
||||
func (h *RolPagesHandler) Create(c *gin.Context) {
|
||||
var req roleService.RolPagesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
created, err := h.service.Create(ctx, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created RolPages", created)
|
||||
}
|
||||
|
||||
// Update godoc
|
||||
//
|
||||
// @Summary Update an existing Role Pages
|
||||
// @Description Update details of an existing Role Pages record by ID
|
||||
// @Tags roles-pages
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Pages ID"
|
||||
// @Param request body roleService.RolPagesRequest true "Payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/{id} [put]
|
||||
func (h *RolPagesHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req roleService.RolPagesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
updated, err := h.service.Update(ctx, id, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully updated RolPages", updated)
|
||||
}
|
||||
|
||||
// Delete godoc
|
||||
//
|
||||
// @Summary Delete a Role Pages
|
||||
// @Description Delete a Role Pages record by ID
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Pages ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/{id} [delete]
|
||||
func (h *RolPagesHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.Delete(ctx, id); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully deleted RolPages", nil)
|
||||
}
|
||||
|
||||
// GetTree godoc
|
||||
//
|
||||
// @Summary Get tree of Role Pages
|
||||
// @Description Retrieve a hierarchical tree of Role Pages
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/tree [get]
|
||||
func (h *RolPagesHandler) GetTree(c *gin.Context) {
|
||||
activeParam := c.Query("active")
|
||||
var activeFilter *bool // default nil (no filter / ambil semua)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeFilter = &isActive
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
tree, err := h.service.GetTree(ctx, activeFilter)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved RolPages", tree)
|
||||
}
|
||||
|
||||
// GetTreeByLevel godoc
|
||||
//
|
||||
// @Summary Get tree of Role Pages by level
|
||||
// @Description Retrieve a hierarchical tree of Role Pages up to a specific level
|
||||
// @Tags roles-pages
|
||||
// @Produce json
|
||||
// @Param level path int true "Hierarchy Level"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/pages/tree/level/{level} [get]
|
||||
func (h *RolPagesHandler) GetTreeByLevel(c *gin.Context) {
|
||||
level, err := strconv.ParseInt(c.Param("level"), 10, 16)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid level format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
activeParam := c.Query("active")
|
||||
var activeFilter *bool // default nil (no filter / ambil semua)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeFilter = &isActive
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
tree, err := h.service.GetTreeByLevel(ctx, int16(level), activeFilter)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved RolPages", tree)
|
||||
}
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
roleService "service/internal/master/role/permission"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/logger"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RolPermissionHandler struct {
|
||||
service roleService.Service
|
||||
}
|
||||
|
||||
func NewRolPermissionHandler(service roleService.Service) *RolPermissionHandler {
|
||||
return &RolPermissionHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *RolPermissionHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/roles/permissions") // get by role
|
||||
{
|
||||
group.GET("", h.GetList)
|
||||
group.GET("/search", h.Search)
|
||||
group.GET("/:id", h.GetDetail) // using role id
|
||||
group.GET("/role/:role", h.GetPermissionTreeRole) // get permission tree by role keycloak
|
||||
group.POST("", h.Create)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.DELETE("/:id", h.Delete)
|
||||
group.GET("/rolemaster/:id", h.GetRolePagesList)
|
||||
}
|
||||
}
|
||||
|
||||
// GetList godoc
|
||||
//
|
||||
// @Summary Get list of Role Permissions
|
||||
// @Description Retrieve a paginated list of Role Permissions
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of items per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields (e.g. +name,-created_at)"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions [get]
|
||||
func (h *RolPermissionHandler) GetList(c *gin.Context) {
|
||||
// Parse Limit & Offset dengan fallback ke page & page_size
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if pageSizeStr := c.Query("limit"); pageSizeStr != "" {
|
||||
limit, _ = strconv.Atoi(pageSizeStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if pageStr := c.Query("offset"); pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 && limit > 0 {
|
||||
offset = (page - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
activeParam := c.Query("active")
|
||||
var activeFilter *bool // default nil (no filter / ambil semua)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeFilter = &isActive
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetList(ctx, limit, offset, sorts, activeFilter)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{Page: page, Limit: limitVal, Total: int(total), TotalPages: totalPages}
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPermission list", data, meta)
|
||||
}
|
||||
|
||||
// GetDetail godoc
|
||||
//
|
||||
// @Summary Get Role Permission detail
|
||||
// @Description Retrieve detailed information about a specific Role Permission
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Permission ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/{id} [get]
|
||||
func (h *RolPermissionHandler) GetDetail(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetDetail(ctx, id)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved RolPermission detail", result)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Search Role Permissions
|
||||
// @Description Search Role Permissions records using dynamic filters
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit per page" default(10)
|
||||
// @Param offset query int false "Offset for pagination" default(0)
|
||||
// @Param sort query string false "Sort fields"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/search [get]
|
||||
func (h *RolPermissionHandler) Search(c *gin.Context) {
|
||||
// Parse Limit & Offset dengan fallback ke page & page_size
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if pageSizeStr := c.Query("limit"); pageSizeStr != "" {
|
||||
limit, _ = strconv.Atoi(pageSizeStr)
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if pageStr := c.Query("offset"); pageStr != "" {
|
||||
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 && limit > 0 {
|
||||
offset = (page - 1) * limit
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil parameter filter secara dinamis
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
for key, values := range c.Request.URL.Query() {
|
||||
if key == "page" || key == "limit" || key == "page_size" || key == "offset" || key == "sort" {
|
||||
continue
|
||||
}
|
||||
if len(values) > 0 && values[0] != "" {
|
||||
filters[key] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parameter sort (format: sort=column1,-column2,+column3)
|
||||
// -column untuk DESC, +column atau column untuk ASC
|
||||
var sorts []string
|
||||
if sortParam := c.Query("sort"); sortParam != "" {
|
||||
sorts = strings.Split(sortParam, ",")
|
||||
// Validasi dan bersihkan sort parameters
|
||||
for i, sort := range sorts {
|
||||
sorts[i] = strings.TrimSpace(sort)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
logger.Default().WithContext(ctx).Info("Search request",
|
||||
logger.String("filters", fmt.Sprintf("%v", filters)),
|
||||
logger.String("sorts", fmt.Sprintf("%v", sorts)),
|
||||
logger.Int("offset", offset),
|
||||
logger.Int("limit", limit))
|
||||
|
||||
// Panggil service dengan parameter sort tambahan
|
||||
result, err := h.service.Search(ctx, filters, sorts, limit, offset)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
// Extract data dari map service untuk response format
|
||||
data := result["data"]
|
||||
total := result["total"].(int64)
|
||||
limitVal := result["limit"].(int)
|
||||
offsetVal := result["offset"].(int)
|
||||
|
||||
page := 1
|
||||
if limitVal > 0 {
|
||||
page = (offsetVal / limitVal) + 1
|
||||
}
|
||||
totalPages := 0
|
||||
if limitVal > 0 {
|
||||
totalPages = int(math.Ceil(float64(total) / float64(limitVal)))
|
||||
}
|
||||
|
||||
meta := response.Meta{
|
||||
Page: page,
|
||||
Limit: limitVal,
|
||||
Total: int(total),
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
|
||||
response.Paginated(c, http.StatusOK, "Successfully retrieved RolPermission search results", data, meta)
|
||||
}
|
||||
|
||||
// GetPermissionTree godoc
|
||||
//
|
||||
// @Summary Get Permission Tree
|
||||
// @Description Retrieve a permission tree
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param role path string true "Role Keycloak"
|
||||
// @Param groups query string false "Comma-separated groups"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/tree/{role} [get]
|
||||
func (h *RolPermissionHandler) GetPermissionTree(c *gin.Context) {
|
||||
roleKeycloak := c.Param("role")
|
||||
|
||||
// Get groups from query parameter (comma-separated)
|
||||
groupsParam := c.Query("groups")
|
||||
var groups []string
|
||||
if groupsParam != "" {
|
||||
groups = strings.Split(groupsParam, ",")
|
||||
// Trim spaces from each group
|
||||
for i, group := range groups {
|
||||
groups[i] = strings.TrimSpace(group)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse active parameter - handle "true", "false", "1", "0"
|
||||
activeParam := c.Query("active")
|
||||
var activeOnly *bool // default nil (no filter)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeOnly = &isActive
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetRolePermissionTree(ctx, roleKeycloak, groups, activeOnly)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
// Jika role disediakan tapi tidak ada data, return empty result
|
||||
if roleKeycloak != "" && (result == nil || len(result.Data.Access) == 0) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "No permissions found for the specified role",
|
||||
"data": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return the result in the desired format
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Successfully retrieved RolPages",
|
||||
"data": result.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPermissionTreeRole godoc
|
||||
//
|
||||
// @Summary Get Permission Tree by Role
|
||||
// @Description Retrieve a permission tree by Keycloak role
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param role path string true "Role Keycloak"
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/role/{role} [get]
|
||||
func (h *RolPermissionHandler) GetPermissionTreeRole(c *gin.Context) {
|
||||
roleKeycloak := c.Param("role")
|
||||
|
||||
// Parse active parameter - handle "true", "false", "1", "0"
|
||||
activeParam := c.Query("active")
|
||||
var activeOnly *bool // default nil (no filter)
|
||||
if activeParam != "" {
|
||||
isActive := activeParam == "true" || activeParam == "1"
|
||||
activeOnly = &isActive
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
result, err := h.service.GetRolePermissionTreeRole(ctx, roleKeycloak, activeOnly)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
// Jika role disediakan tapi tidak ada data, return empty result
|
||||
if roleKeycloak != "" && (result == nil || len(result.Data.Access) == 0) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "No permissions found for the specified role",
|
||||
"data": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return the result in the desired format
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Successfully retrieved RolPages",
|
||||
"data": result.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// Create godoc
|
||||
//
|
||||
// @Summary Create new Role Permission
|
||||
// @Description Create a new Role Permission record
|
||||
// @Tags roles-permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body roleService.RolPermissionRequest true "Payload"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions [post]
|
||||
func (h *RolPermissionHandler) Create(c *gin.Context) {
|
||||
var req roleService.RolPermissionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
created, err := h.service.Create(ctx, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created RolPermission", created)
|
||||
}
|
||||
|
||||
// Update godoc
|
||||
//
|
||||
// @Summary Update an existing Role Permission
|
||||
// @Description Update details of an existing Role Permission record by ID
|
||||
// @Tags roles-permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Permission ID"
|
||||
// @Param request body roleService.RolPermissionRequest true "Payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/{id} [put]
|
||||
func (h *RolPermissionHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req roleService.RolPermissionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
updated, err := h.service.Update(ctx, id, req)
|
||||
if err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully updated RolPermission", updated)
|
||||
}
|
||||
|
||||
// Delete godoc
|
||||
//
|
||||
// @Summary Delete a Role Permission
|
||||
// @Description Delete a Role Permission record by ID
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param id path int true "Role Permission ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Security BearerAuth
|
||||
// @Router /roles/permissions/{id} [delete]
|
||||
func (h *RolPermissionHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Invalid ID format", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if err := h.service.Delete(ctx, id); err != nil {
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, http.StatusOK, "Successfully deleted RolPermission", nil)
|
||||
}
|
||||
|
||||
// GetRolePagesList mengambil daftar role beserta page dan permission-nya
|
||||
// @Summary Get Role Pages Access List
|
||||
// @Description Retrieve a paginated list of RolePages grouped by Role with Tree Hierarchy
|
||||
// @Tags roles-permissions
|
||||
// @Produce json
|
||||
// @Param id path string true "Role Master ID"
|
||||
// @Param page query integer false "Page number" default(1)
|
||||
// @Param limit query integer false "Items per page" default(10)
|
||||
// @Param active query boolean false "Filter by active status"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /roles/permissions/rolemaster/{id} [get]
|
||||
func (h *RolPermissionHandler) GetRolePagesList(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
roleID := c.Param("id")
|
||||
|
||||
// 1. Ambil Parameter Pagination
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit < -1 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
if limit == -1 {
|
||||
offset = 0 // Jika limit -1, fetch all data
|
||||
}
|
||||
|
||||
// 2. Ambil Parameter Filter Active
|
||||
defaultActive := true
|
||||
activeFilter := &defaultActive
|
||||
if activeStr := c.Query("active"); activeStr != "" {
|
||||
parsedActive, err := strconv.ParseBool(activeStr)
|
||||
if err == nil {
|
||||
activeFilter = &parsedActive
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Panggil Service layer
|
||||
res, err := h.service.GetRolePagesList(ctx, roleID, limit, offset, activeFilter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Kalkulasi Metadata & Response Sesuai Format
|
||||
total := res["total"].(int64)
|
||||
totalPages := 1
|
||||
if limit > 0 {
|
||||
totalPages = int((total + int64(limit) - 1) / int64(limit))
|
||||
}
|
||||
|
||||
// Format JSON Response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Successfully retrieved RolPages list",
|
||||
"data": res["data"],
|
||||
"meta": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"total_pages": totalPages,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
stdErrors "errors"
|
||||
"net/http"
|
||||
"service/internal/interfaces/satusehat"
|
||||
"service/internal/satusehat/reference/auth"
|
||||
"service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthHandler menangani endpoint HTTP untuk Auth Satu Sehat.
|
||||
type AuthHandler struct {
|
||||
service auth.Service
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
|
||||
func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference")
|
||||
{
|
||||
group.GET("/auth/token", h.GetToken)
|
||||
group.POST("/auth/token/refresh", h.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
// NewAuthHandler membuat instance baru dari AuthHandler.
|
||||
func NewAuthHandler(service auth.Service) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// handleSatuSehatError mengekstrak OperationOutcome agar JSON bisa tampil terstruktur di response
|
||||
func handleSatuSehatError(c *gin.Context, err error) {
|
||||
var ssErr *satusehat.ErrorOperationOutcome
|
||||
if stdErrors.As(err, &ssErr) {
|
||||
c.JSON(ssErr.StatusCode, gin.H{
|
||||
"status": "error",
|
||||
"message": ssErr.Outcome,
|
||||
"error": gin.H{
|
||||
"severity": "error",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
appErr := errors.FromError(err)
|
||||
response.Error(c, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
}
|
||||
|
||||
// GetToken godoc
|
||||
//
|
||||
// @Summary Get SatuSehat Access Token
|
||||
// @Description Mendapatkan token aktif untuk API Satu Sehat Kemenkes (menggunakan cache internal)
|
||||
// @Tags Satu Sehat - Auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /satusehat/reference/auth/token [get]
|
||||
// @Security BearerAuth
|
||||
func (h *AuthHandler) GetToken(c *gin.Context) {
|
||||
data, err := h.service.GetToken(c.Request.Context())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan token Satu Sehat", data)
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
//
|
||||
// @Summary Refresh SatuSehat Access Token
|
||||
// @Description Memaksa request token baru dari API Satu Sehat Kemenkes (bypass cache)
|
||||
// @Tags Satu Sehat - Auth
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 500 {object} response.Response
|
||||
// @Router /satusehat/reference/auth/token/refresh [post]
|
||||
// @Security BearerAuth
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
data, err := h.service.RefreshToken(c.Request.Context())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil me-refresh token Satu Sehat", data)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/reference/kfa"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type KFAHandler struct {
|
||||
service kfa.Service
|
||||
}
|
||||
|
||||
func NewKFAHandler(service kfa.Service) *KFAHandler {
|
||||
return &KFAHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetByCode godoc
|
||||
//
|
||||
// @Summary Cari Produk KFA berdasarkan Kode
|
||||
// @Description Mencari detail produk farmasi/alkes dari API Kamus Farmasi dan Alat Kesehatan (KFA) Satu Sehat
|
||||
// @Tags Satu Sehat - KFA
|
||||
// @Produce json
|
||||
// @Param code path string true "Kode KFA (contoh: 93000469)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/kfa/products/{code} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *KFAHandler) GetByCode(c *gin.Context) {
|
||||
code := c.Param("code")
|
||||
data, err := h.service.GetByCode(c.Request.Context(), code)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data produk KFA", data)
|
||||
}
|
||||
|
||||
// GetProducts godoc
|
||||
//
|
||||
// @Summary Daftar Produk KFA
|
||||
// @Description Mengambil daftar produk KFA dengan kapabilitas paginasi
|
||||
// @Tags Satu Sehat - KFA
|
||||
// @Produce json
|
||||
// @Param page query int false "Nomor Halaman (default: 1)"
|
||||
// @Param size query int false "Jumlah Data (default: 10)"
|
||||
// @Param product_type query string false "Tipe Produk (farmasi/alkes)"
|
||||
// @Param keyword query string false "Kata Kunci Pencarian"
|
||||
// @Param from_ query string false "Parameter waktu (from_)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/kfa/products [get]
|
||||
// @Security BearerAuth
|
||||
func (h *KFAHandler) GetProducts(c *gin.Context) {
|
||||
var params kfa.KFASearchParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format parameter tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.GetProducts(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mengambil daftar produk KFA", data)
|
||||
}
|
||||
|
||||
func (h *KFAHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference/kfa")
|
||||
{
|
||||
group.GET("/products/:code", h.GetByCode)
|
||||
group.GET("/products", h.GetProducts)
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/reference/location"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LocationHandler struct {
|
||||
service location.Service
|
||||
}
|
||||
|
||||
func NewLocationHandler(service location.Service) *LocationHandler {
|
||||
return &LocationHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetByID godoc
|
||||
//
|
||||
// @Summary Cari Lokasi (Satu Sehat) berdasarkan ID
|
||||
// @Description Mencari data Location FHIR Satu Sehat berdasarkan ID
|
||||
// @Tags Satu Sehat - Location
|
||||
// @Produce json
|
||||
// @Param id path string true "Location ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/location/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *LocationHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
data, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data lokasi", data)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Pencarian Lokasi (Satu Sehat)
|
||||
// @Description Mencari data Location berdasarkan parameter
|
||||
// @Tags Satu Sehat - Location
|
||||
// @Produce json
|
||||
// @Param name query string false "Nama Lokasi"
|
||||
// @Param organization query string false "ID Organisasi Pemilik"
|
||||
// @Param identifier query string false "Identifier Lokasi"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/location [get]
|
||||
// @Security BearerAuth
|
||||
func (h *LocationHandler) Search(c *gin.Context) {
|
||||
var params location.LocationSearchParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Search(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data lokasi", data)
|
||||
}
|
||||
|
||||
func (h *LocationHandler) Create(c *gin.Context) {
|
||||
var payload map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Create(c.Request.Context(), payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Berhasil membuat data lokasi", data)
|
||||
}
|
||||
|
||||
func (h *LocationHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var payload map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Update(c.Request.Context(), id, payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mengubah data lokasi", data)
|
||||
}
|
||||
|
||||
func (h *LocationHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var payload interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Patch(c.Request.Context(), id, payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil melakukan patch data lokasi", data)
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
|
||||
func (h *LocationHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference")
|
||||
{
|
||||
group.GET("/location/:id", h.GetByID)
|
||||
group.GET("/location", h.Search)
|
||||
group.POST("/location", h.Create)
|
||||
group.PUT("/location/:id", h.Update)
|
||||
group.PATCH("/location/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/reference/organization"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OrganizationHandler struct {
|
||||
service organization.Service
|
||||
}
|
||||
|
||||
func NewOrganizationHandler(service organization.Service) *OrganizationHandler {
|
||||
return &OrganizationHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetByID godoc
|
||||
//
|
||||
// @Summary Cari Organisasi (Satu Sehat) berdasarkan ID
|
||||
// @Description Mencari data Organization FHIR Satu Sehat berdasarkan ID
|
||||
// @Tags Satu Sehat - Organization
|
||||
// @Produce json
|
||||
// @Param id path string true "Organization ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/organization/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *OrganizationHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
data, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data organisasi", data)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Pencarian Organisasi (Satu Sehat)
|
||||
// @Description Mencari data Organization berdasarkan Name atau PartOf
|
||||
// @Tags Satu Sehat - Organization
|
||||
// @Produce json
|
||||
// @Param name query string false "Nama Organisasi"
|
||||
// @Param partof query string false "ID Organisasi Induk"
|
||||
// @Param identifier query string false "Identifier Organisasi"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/organization [get]
|
||||
// @Security BearerAuth
|
||||
func (h *OrganizationHandler) Search(c *gin.Context) {
|
||||
var params organization.OrganizationSearchParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validasi mandiri sebelum menembak ke API Kemenkes
|
||||
if params.Name == "" && params.PartOf == "" && params.Identifier == "" {
|
||||
response.Error(c, http.StatusBadRequest, "Parameter pencarian tidak lengkap", "Harap masukkan minimal salah satu parameter query: name, partof, atau identifier")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Search(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data organisasi", data)
|
||||
}
|
||||
|
||||
func (h *OrganizationHandler) Create(c *gin.Context) {
|
||||
var payload map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Create(c.Request.Context(), payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Berhasil membuat data organisasi", data)
|
||||
}
|
||||
|
||||
func (h *OrganizationHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var payload map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Update(c.Request.Context(), id, payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mengubah data organisasi", data)
|
||||
}
|
||||
|
||||
func (h *OrganizationHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// JSON Patch biasanya array of objects, jadi gunakan interface{} general
|
||||
var payload interface{}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Patch(c.Request.Context(), id, payload)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil melakukan patch data organisasi", data)
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
|
||||
func (h *OrganizationHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference")
|
||||
{
|
||||
group.GET("/organization/:id", h.GetByID)
|
||||
group.GET("/organization", h.Search)
|
||||
group.POST("/organization", h.Create)
|
||||
group.PUT("/organization/:id", h.Update)
|
||||
group.PATCH("/organization/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"service/internal/satusehat/reference/patient"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PatientHandler menangani endpoint HTTP untuk resource Patient Satu Sehat.
|
||||
type PatientHandler struct {
|
||||
service patient.Service
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
|
||||
func (h *PatientHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference")
|
||||
{
|
||||
group.GET("/patient/nik/:nik", h.GetByNIK)
|
||||
group.GET("/patient/:id", h.GetByID)
|
||||
group.GET("/patient", h.Search)
|
||||
group.POST("/patient", h.Create)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatientHandler membuat instance baru dari PatientHandler.
|
||||
func NewPatientHandler(service patient.Service) *PatientHandler {
|
||||
return &PatientHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetByNIK godoc
|
||||
//
|
||||
// @Summary Cari Pasien (Satu Sehat) berdasarkan NIK
|
||||
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan Nomor Induk Kependudukan (NIK)
|
||||
// @Tags Satu Sehat - Patient
|
||||
// @Produce json
|
||||
// @Param nik path string true "Nomor Induk Kependudukan"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/patient/nik/{nik} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PatientHandler) GetByNIK(c *gin.Context) {
|
||||
nik := c.Param("nik")
|
||||
data, err := h.service.GetByNIK(c.Request.Context(), nik)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
|
||||
}
|
||||
|
||||
// GetByID godoc
|
||||
//
|
||||
// @Summary Cari Pasien (Satu Sehat) berdasarkan ID
|
||||
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan IHS Number / ID
|
||||
// @Tags Satu Sehat - Patient
|
||||
// @Produce json
|
||||
// @Param id path string true "IHS Number / Patient ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/patient/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PatientHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
data, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Pencarian Pasien (Satu Sehat) Multi-Parameter
|
||||
// @Description Mencari data pasien FHIR Satu Sehat berdasarkan kombinasi parameter (Nama, NIK, NIK Ibu, Tanggal Lahir, Gender)
|
||||
// @Tags Satu Sehat - Patient
|
||||
// @Produce json
|
||||
// @Param nik query string false "Nomor Induk Kependudukan"
|
||||
// @Param nik_ibu query string false "Nomor Induk Kependudukan Ibu (Untuk bayi)"
|
||||
// @Param name query string false "Nama Pasien"
|
||||
// @Param birthdate query string false "Tanggal Lahir (YYYY-MM-DD)"
|
||||
// @Param gender query string false "Jenis Kelamin (male/female)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/patient [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PatientHandler) Search(c *gin.Context) {
|
||||
var params patient.PatientSearchParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Search(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data pasien", data)
|
||||
}
|
||||
|
||||
// Create godoc
|
||||
//
|
||||
// @Summary Daftar Pasien Baru (Satu Sehat)
|
||||
// @Description Mendaftarkan data pasien baru ke API Satu Sehat dan mengembalikan IHS Number
|
||||
// @Tags Satu Sehat - Patient
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body patient.CreatePatientRequest true "Data Pasien Baru"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Router /satusehat/reference/patient [post]
|
||||
// @Security BearerAuth
|
||||
func (h *PatientHandler) Create(c *gin.Context) {
|
||||
var req patient.CreatePatientRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Berhasil mendaftarkan pasien", data)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package reference
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/reference/practitioner"
|
||||
"service/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PractitionerHandler struct {
|
||||
service practitioner.Service
|
||||
}
|
||||
|
||||
func NewPractitionerHandler(service practitioner.Service) *PractitionerHandler {
|
||||
return &PractitionerHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetByNIK godoc
|
||||
//
|
||||
// @Summary Cari Tenaga Medis (Satu Sehat) berdasarkan NIK
|
||||
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan NIK
|
||||
// @Tags Satu Sehat - Practitioner
|
||||
// @Produce json
|
||||
// @Param nik path string true "Nomor Induk Kependudukan"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/practitioner/nik/{nik} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PractitionerHandler) GetByNIK(c *gin.Context) {
|
||||
nik := c.Param("nik")
|
||||
data, err := h.service.GetByNIK(c.Request.Context(), nik)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
|
||||
}
|
||||
|
||||
// GetByID godoc
|
||||
//
|
||||
// @Summary Cari Tenaga Medis (Satu Sehat) berdasarkan ID
|
||||
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan IHS Number / ID
|
||||
// @Tags Satu Sehat - Practitioner
|
||||
// @Produce json
|
||||
// @Param id path string true "IHS Number / Practitioner ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/practitioner/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PractitionerHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
data, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
//
|
||||
// @Summary Pencarian Tenaga Medis (Satu Sehat) Multi-Parameter
|
||||
// @Description Mencari data Practitioner FHIR Satu Sehat berdasarkan parameter
|
||||
// @Tags Satu Sehat - Practitioner
|
||||
// @Produce json
|
||||
// @Param nik query string false "Nomor Induk Kependudukan"
|
||||
// @Param name query string false "Nama Tenaga Medis"
|
||||
// @Param gender query string false "Jenis Kelamin (male/female)"
|
||||
// @Param birthdate query string false "Tanggal Lahir (YYYY-MM-DD)"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /satusehat/reference/practitioner [get]
|
||||
// @Security BearerAuth
|
||||
func (h *PractitionerHandler) Search(c *gin.Context) {
|
||||
var params practitioner.PractitionerSearchParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "Format pencarian tidak valid", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.service.Search(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Berhasil mendapatkan data tenaga medis", data)
|
||||
}
|
||||
|
||||
// RegisterRoutes mendaftarkan endpoint handler ini ke router Gin
|
||||
func (h *PractitionerHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/reference")
|
||||
{
|
||||
group.GET("/practitioner/nik/:nik", h.GetByNIK)
|
||||
group.GET("/practitioner/:id", h.GetByID)
|
||||
group.GET("/practitioner", h.Search)
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"service/internal/satusehat/usecase/allergyintolerance"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AllergyIntoleranceHandler struct{ service allergyintolerance.Service }
|
||||
|
||||
func NewAllergyIntoleranceHandler(s allergyintolerance.Service) *AllergyIntoleranceHandler {
|
||||
return &AllergyIntoleranceHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *AllergyIntoleranceHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
g := router.Group("/satusehat/allergyintolerance")
|
||||
g.POST("", h.Create)
|
||||
g.GET("", h.Search)
|
||||
g.GET("/:id", h.GetByID)
|
||||
g.PUT("/:id", h.Update)
|
||||
g.PATCH("/:id", h.Patch)
|
||||
}
|
||||
|
||||
func (h *AllergyIntoleranceHandler) Create(c *gin.Context) {
|
||||
var req allergyintolerance.AllergyIntoleranceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *AllergyIntoleranceHandler) Update(c *gin.Context) {
|
||||
var req allergyintolerance.AllergyIntoleranceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *AllergyIntoleranceHandler) Patch(c *gin.Context) {
|
||||
var req allergyintolerance.AllergyIntolerancePatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *AllergyIntoleranceHandler) GetByID(c *gin.Context) {
|
||||
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *AllergyIntoleranceHandler) Search(c *gin.Context) {
|
||||
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"service/internal/satusehat/usecase/careplan"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CarePlanHandler struct{ service careplan.Service }
|
||||
|
||||
func NewCarePlanHandler(s careplan.Service) *CarePlanHandler { return &CarePlanHandler{service: s} }
|
||||
|
||||
func (h *CarePlanHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
g := router.Group("/satusehat/careplan")
|
||||
g.POST("", h.Create)
|
||||
g.GET("", h.Search)
|
||||
g.GET("/:id", h.GetByID)
|
||||
g.PUT("/:id", h.Update)
|
||||
g.PATCH("/:id", h.Patch)
|
||||
}
|
||||
|
||||
func (h *CarePlanHandler) Create(c *gin.Context) {
|
||||
var req careplan.CarePlanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *CarePlanHandler) Update(c *gin.Context) {
|
||||
var req careplan.CarePlanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *CarePlanHandler) Patch(c *gin.Context) {
|
||||
var req careplan.CarePlanPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *CarePlanHandler) GetByID(c *gin.Context) {
|
||||
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *CarePlanHandler) Search(c *gin.Context) {
|
||||
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
clinicalimpression "service/internal/satusehat/usecase/clinicalImpression"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ClinicalImpressionHandler struct{ service clinicalimpression.Service }
|
||||
|
||||
func NewClinicalImpressionHandler(s clinicalimpression.Service) *ClinicalImpressionHandler {
|
||||
return &ClinicalImpressionHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *ClinicalImpressionHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
g := router.Group("/satusehat/clinicalimpression")
|
||||
g.POST("", h.Create)
|
||||
g.GET("", h.Search)
|
||||
g.GET("/:id", h.GetByID)
|
||||
g.PUT("/:id", h.Update)
|
||||
g.PATCH("/:id", h.Patch)
|
||||
}
|
||||
|
||||
func (h *ClinicalImpressionHandler) Create(c *gin.Context) {
|
||||
var req clinicalimpression.ClinicalImpressionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ClinicalImpressionHandler) Update(c *gin.Context) {
|
||||
var req clinicalimpression.ClinicalImpressionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ClinicalImpressionHandler) Patch(c *gin.Context) {
|
||||
var req clinicalimpression.ClinicalImpressionPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ClinicalImpressionHandler) GetByID(c *gin.Context) {
|
||||
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ClinicalImpressionHandler) Search(c *gin.Context) {
|
||||
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/composition"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CompositionHandler struct {
|
||||
service composition.Service
|
||||
}
|
||||
|
||||
func NewCompositionHandler(service composition.Service) *CompositionHandler {
|
||||
return &CompositionHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/composition")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) Create(c *gin.Context) {
|
||||
var req composition.CompositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created Composition", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req composition.CompositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Composition", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req composition.CompositionPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Composition", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Composition", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *CompositionHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Compositions", result.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/condition"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConditionHandler struct {
|
||||
service condition.Service
|
||||
}
|
||||
|
||||
func NewConditionHandler(service condition.Service) *ConditionHandler {
|
||||
return &ConditionHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/condition")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) Create(c *gin.Context) {
|
||||
var req condition.ConditionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created Condition", result)
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req condition.ConditionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Condition", result)
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req condition.ConditionPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Condition", result)
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Condition", result)
|
||||
}
|
||||
|
||||
func (h *ConditionHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Conditions", result)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"service/internal/satusehat/usecase/diagnosticreport"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DiagnosticReportHandler struct{ service diagnosticreport.Service }
|
||||
|
||||
func NewDiagnosticReportHandler(s diagnosticreport.Service) *DiagnosticReportHandler {
|
||||
return &DiagnosticReportHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *DiagnosticReportHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
g := router.Group("/satusehat/diagnosticreport")
|
||||
g.POST("", h.Create)
|
||||
g.GET("", h.Search)
|
||||
g.GET("/:id", h.GetByID)
|
||||
g.PUT("/:id", h.Update)
|
||||
g.PATCH("/:id", h.Patch)
|
||||
}
|
||||
|
||||
func (h *DiagnosticReportHandler) Create(c *gin.Context) {
|
||||
var req diagnosticreport.DiagnosticReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *DiagnosticReportHandler) Update(c *gin.Context) {
|
||||
var req diagnosticreport.DiagnosticReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *DiagnosticReportHandler) Patch(c *gin.Context) {
|
||||
var req diagnosticreport.DiagnosticReportPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *DiagnosticReportHandler) GetByID(c *gin.Context) {
|
||||
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *DiagnosticReportHandler) Search(c *gin.Context) {
|
||||
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"service/internal/interfaces/satusehat"
|
||||
"service/internal/satusehat/usecase/encounter"
|
||||
pkgErrors "service/pkg/errors"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type EncounterHandler struct {
|
||||
service encounter.Service
|
||||
}
|
||||
|
||||
func NewEncounterHandler(service encounter.Service) *EncounterHandler {
|
||||
return &EncounterHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/encounter")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSatuSehatError mengekstrak OperationOutcome agar JSON bisa tampil terstruktur
|
||||
func handleSatuSehatError(c *gin.Context, err error) {
|
||||
var ssErr *satusehat.ErrorOperationOutcome
|
||||
if errors.As(err, &ssErr) {
|
||||
c.Error(err) // Log error asli
|
||||
c.JSON(ssErr.StatusCode, gin.H{
|
||||
"status": "error",
|
||||
"message": ssErr.Outcome,
|
||||
"error": gin.H{
|
||||
"severity": "error",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
appErr := pkgErrors.FromError(err)
|
||||
response.ErrorWithLog(c, err, appErr.HTTPStatus(), appErr.Error(), appErr.Metadata())
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) Create(c *gin.Context) {
|
||||
var req encounter.EncounterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Di sini, Anda bisa menyimpan result.RawResponse atau result.ID ke database log jika diperlukan.
|
||||
// Contoh:
|
||||
// go logService.Create(c.Request.Context(), "encounter_create", req, result.RawResponse, http.StatusCreated)
|
||||
|
||||
response.Success(c, http.StatusCreated, "Successfully created Encounter", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req encounter.EncounterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Encounter", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req encounter.EncounterPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Encounter", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Encounter", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) Search(c *gin.Context) {
|
||||
// Get query parameters string natively and fetch it directly (like: ?patient=xxx&status=active)
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Encounters", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
|
||||
idxdaftarStr := c.Param("idxdaftar")
|
||||
idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
|
||||
if err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/episodeofcare"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type EpisodeOfCareHandler struct {
|
||||
service episodeofcare.Service
|
||||
}
|
||||
|
||||
func NewEpisodeOfCareHandler(service episodeofcare.Service) *EpisodeOfCareHandler {
|
||||
return &EpisodeOfCareHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/episodeofcare")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) Create(c *gin.Context) {
|
||||
var req episodeofcare.EpisodeOfCareRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created EpisodeOfCare", result)
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req episodeofcare.EpisodeOfCareRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated EpisodeOfCare", result)
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req episodeofcare.EpisodeOfCarePatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched EpisodeOfCare", result)
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved EpisodeOfCare", result)
|
||||
}
|
||||
|
||||
func (h *EpisodeOfCareHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved EpisodeOfCare records", result)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/imagingstudy"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ImagingStudyHandler struct {
|
||||
service imagingstudy.Service
|
||||
}
|
||||
|
||||
func NewImagingStudyHandler(service imagingstudy.Service) *ImagingStudyHandler {
|
||||
return &ImagingStudyHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/imagingstudy")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) Create(c *gin.Context) {
|
||||
var req imagingstudy.ImagingStudyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created ImagingStudy", result)
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req imagingstudy.ImagingStudyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated ImagingStudy", result)
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req imagingstudy.ImagingStudyPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched ImagingStudy", result)
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved ImagingStudy", result)
|
||||
}
|
||||
|
||||
func (h *ImagingStudyHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved ImagingStudies", result)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"service/internal/satusehat/usecase/immunization"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ImmunizationHandler struct{ service immunization.Service }
|
||||
|
||||
func NewImmunizationHandler(s immunization.Service) *ImmunizationHandler {
|
||||
return &ImmunizationHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *ImmunizationHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
g := router.Group("/satusehat/immunization")
|
||||
g.POST("", h.Create)
|
||||
g.GET("", h.Search)
|
||||
g.GET("/:id", h.GetByID)
|
||||
g.PUT("/:id", h.Update)
|
||||
g.PATCH("/:id", h.Patch)
|
||||
}
|
||||
|
||||
func (h *ImmunizationHandler) Create(c *gin.Context) {
|
||||
var req immunization.ImmunizationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ImmunizationHandler) Update(c *gin.Context) {
|
||||
var req immunization.ImmunizationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ImmunizationHandler) Patch(c *gin.Context) {
|
||||
var req immunization.ImmunizationPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ImmunizationHandler) GetByID(c *gin.Context) {
|
||||
res, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
func (h *ImmunizationHandler) Search(c *gin.Context) {
|
||||
res, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Success", res.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/medication"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MedicationHandler struct {
|
||||
service medication.Service
|
||||
}
|
||||
|
||||
func NewMedicationHandler(service medication.Service) *MedicationHandler {
|
||||
return &MedicationHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/medication")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) Create(c *gin.Context) {
|
||||
var req medication.MedicationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created Medication", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medication.MedicationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Medication", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medication.MedicationPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Medication", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Medication", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Medications", result.FullResponse)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/medicationdispense"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MedicationDispenseHandler struct {
|
||||
service medicationdispense.Service
|
||||
}
|
||||
|
||||
func NewMedicationDispenseHandler(service medicationdispense.Service) *MedicationDispenseHandler {
|
||||
return &MedicationDispenseHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *MedicationDispenseHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/medicationdispense")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MedicationDispenseHandler) Create(c *gin.Context) {
|
||||
var req medicationdispense.MedicationDispenseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created MedicationDispense", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationDispenseHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationdispense.MedicationDispenseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated MedicationDispense", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationDispenseHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationdispense.MedicationDispensePatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched MedicationDispense", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationDispenseHandler) GetByID(c *gin.Context) {
|
||||
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
func (h *MedicationDispenseHandler) Search(c *gin.Context) {
|
||||
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/medicationrequest"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MedicationRequestHandler struct {
|
||||
service medicationrequest.Service
|
||||
}
|
||||
|
||||
func NewMedicationRequestHandler(service medicationrequest.Service) *MedicationRequestHandler {
|
||||
return &MedicationRequestHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *MedicationRequestHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/medicationrequest")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MedicationRequestHandler) Create(c *gin.Context) {
|
||||
var req medicationrequest.MedicationRequestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created MedicationRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationRequestHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationrequest.MedicationRequestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated MedicationRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationRequestHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationrequest.MedicationRequestPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched MedicationRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationRequestHandler) GetByID(c *gin.Context) {
|
||||
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
func (h *MedicationRequestHandler) Search(c *gin.Context) {
|
||||
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/medicationstatement"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MedicationStatementHandler struct {
|
||||
service medicationstatement.Service
|
||||
}
|
||||
|
||||
func NewMedicationStatementHandler(service medicationstatement.Service) *MedicationStatementHandler {
|
||||
return &MedicationStatementHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *MedicationStatementHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/medicationstatement")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MedicationStatementHandler) Create(c *gin.Context) {
|
||||
var req medicationstatement.MedicationStatementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created MedicationStatement", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationStatementHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationstatement.MedicationStatementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated MedicationStatement", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationStatementHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req medicationstatement.MedicationStatementPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched MedicationStatement", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *MedicationStatementHandler) GetByID(c *gin.Context) {
|
||||
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
func (h *MedicationStatementHandler) Search(c *gin.Context) {
|
||||
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/observation"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ObservationHandler struct {
|
||||
service observation.Service
|
||||
}
|
||||
|
||||
func NewObservationHandler(service observation.Service) *ObservationHandler {
|
||||
return &ObservationHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/observation")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) Create(c *gin.Context) {
|
||||
var req observation.ObservationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created Observation", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req observation.ObservationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Observation", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req observation.ObservationPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Observation", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Observation", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ObservationHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Observations", result.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/procedure"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ProcedureHandler struct {
|
||||
service procedure.Service
|
||||
}
|
||||
|
||||
func NewProcedureHandler(service procedure.Service) *ProcedureHandler {
|
||||
return &ProcedureHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/procedure")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) Create(c *gin.Context) {
|
||||
var req procedure.ProcedureRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created Procedure", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req procedure.ProcedureRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated Procedure", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req procedure.ProcedurePatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched Procedure", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Procedure", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ProcedureHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved Procedures", result.FullResponse)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/questionnaireresponse"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type QuestionnaireResponseHandler struct {
|
||||
service questionnaireresponse.Service
|
||||
}
|
||||
|
||||
func NewQuestionnaireResponseHandler(service questionnaireresponse.Service) *QuestionnaireResponseHandler {
|
||||
return &QuestionnaireResponseHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *QuestionnaireResponseHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/questionnaireresponse")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestionnaireResponseHandler) Create(c *gin.Context) {
|
||||
var req questionnaireresponse.QuestionnaireResponseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created QuestionnaireResponse", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *QuestionnaireResponseHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req questionnaireresponse.QuestionnaireResponseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated QuestionnaireResponse", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *QuestionnaireResponseHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req questionnaireresponse.QuestionnaireResponsePatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
|
||||
return
|
||||
}
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched QuestionnaireResponse", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *QuestionnaireResponseHandler) GetByID(c *gin.Context) {
|
||||
result, err := h.service.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
func (h *QuestionnaireResponseHandler) Search(c *gin.Context) {
|
||||
result, err := h.service.Search(c.Request.Context(), c.Request.URL.Query())
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved", result.FullResponse)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"service/internal/satusehat/usecase/servicerequest"
|
||||
"service/pkg/response"
|
||||
"service/pkg/utils/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServiceRequestHandler struct {
|
||||
service servicerequest.Service
|
||||
}
|
||||
|
||||
func NewServiceRequestHandler(service servicerequest.Service) *ServiceRequestHandler {
|
||||
return &ServiceRequestHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/satusehat/servicerequest")
|
||||
{
|
||||
group.POST("", h.Create)
|
||||
group.GET("", h.Search)
|
||||
group.GET("/:id", h.GetByID)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id", h.Patch)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) Create(c *gin.Context) {
|
||||
var req servicerequest.ServiceRequestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusCreated, "Successfully created ServiceRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req servicerequest.ServiceRequestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully updated ServiceRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) Patch(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req servicerequest.ServiceRequestPatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
customErr := validator.TranslateError(err)
|
||||
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", customErr)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Patch(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully patched ServiceRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved ServiceRequest", result.FullResponse)
|
||||
}
|
||||
|
||||
func (h *ServiceRequestHandler) Search(c *gin.Context) {
|
||||
queryParams := c.Request.URL.Query()
|
||||
result, err := h.service.Search(c.Request.Context(), queryParams)
|
||||
if err != nil {
|
||||
handleSatuSehatError(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, http.StatusOK, "Successfully retrieved ServiceRequests", result.FullResponse)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user