first commit

This commit is contained in:
meninjar
2026-04-14 01:23:34 +00:00
commit edfaa886ff
443 changed files with 1245931 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main cmd/api/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
+168
View File
@@ -0,0 +1,168 @@
# Server Application
APP_ENV=development
GIN_MODE=debug
# Tambahkan di .env file
LOGGER_LEVEL=debug
LOGGER_FORMAT=text
# SERVER_TYPE=rest # This will enable the HTTP REST server
SERVER_PORT=8096
AUTH_TYPE=hybrid
# # Multiple Static Tokens (comma-separated list)
AUTH_STATIC_TOKENS=token5,token6,token7,token8
# # Fallback authentication for hybrid mode
AUTH_FALLBACK_TO=jwt
# # Keycloak Configuration (optional)
KEYCLOAK_CLIENT_ID=farmasi
KEYCLOAK_CLIENT_SECRET=6ttxZK76N8Q6eCk9BaxHiWOrIXECgNAz
KEYCLOAK_ISSUER=https://auth.rssa.top/realms/Dev
KEYCLOAK_URL=https://auth.rssa.top
KEYCLOAK_REALM=Dev
KEYCLOAK_AUDIENCE=farmasi
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/Dev/protocol/openid-connect/certs
KEYCLOAK_ENABLED=true
# POSTGRES_DEFAULT_HOST=10.10.123.206
# POSTGRES_DEFAULT_PORT=5432
# POSTGRES_DEFAULT_USERNAME=postgres
# POSTGRES_DEFAULT_PASSWORD=rss@j@y@2025
# POSTGRES_DEFAULT_DATABASE=health
# POSTGRES_DEFAULT_SSLMODE=disable
# # Timeout koneksi database
# POSTGRES_DEFAULT_TIMEOUT=5s
POSTGRES_DEFAULT_HOST=10.10.123.198
POSTGRES_DEFAULT_PORT=5100
POSTGRES_DEFAULT_USERNAME=stim_farmasi_app
POSTGRES_DEFAULT_PASSWORD=mailadmin123
POSTGRES_DEFAULT_DATABASE=db_farmasi
POSTGRES_DEFAULT_SSLMODE=disable
# Timeout koneksi database
POSTGRES_DEFAULT_TIMEOUT=5s
# POSTGRES_DEFAULT_HOST=10.10.123.198
# POSTGRES_DEFAULT_PORT=5100
# POSTGRES_DEFAULT_USERNAME=stim_read
# POSTGRES_DEFAULT_PASSWORD=itikom123
# POSTGRES_DEFAULT_DATABASE=db_antrean_operasi
# POSTGRES_DEFAULT_SSLMODE=disable
# # Timeout koneksi database
# POSTGRES_DEFAULT_TIMEOUT=5s
POSTGRES_SIMRS_HOST=10.10.123.238
POSTGRES_SIMRS_PORT=5432
POSTGRES_SIMRS_USERNAME=brawijaya
POSTGRES_SIMRS_PASSWORD=ub*2025
POSTGRES_SIMRS_DATABASE=simrs
POSTGRES_SIMRS_SSLMODE=disable
POSTGRES_SIMRS_TIMEOUT=5s
POSTGRES_SATUDATA_CONNECTION=postgres
POSTGRES_SATUDATA_HOST=10.10.123.165
POSTGRES_SATUDATA_PORT=5000
POSTGRES_SATUDATA_USERNAME=stim
POSTGRES_SATUDATA_PASSWORD=stim*RS54
POSTGRES_SATUDATA_DATABASE=satu_db
POSTGRES_SATUDATA_SSLMODE=disable
POSTGRES_SATUDATA_TIMEOUT=5s
# DB_CONNECTION=pgsql
# DB_HOST=10.10.123.198
# DB_PORT=5100
# DB_DATABASE=db_eticket_farmasi
# DB_USERNAME=stim_eticket_app
# DB_PASSWORD=mailadmin123
SQLSERVER_FARMASI_CONNECTION=sqlserver
SQLSERVER_FARMASI_USERNAME=komfar
SQLSERVER_FARMASI_PASSWORD=Komfar123
SQLSERVER_FARMASI_HOST=10.10.90.232
SQLSERVER_FARMASI_DATABASE=DBINV
SQLSERVER_FARMASI_PORT=1433
SQLSERVER_FARMASI_SSLMODE=disable
SQLSERVER_FARMASI_TIMEOUT=5s
MYSQL_RIS_CONNECTION=mysql
MYSQL_RIS_USERNAME=admin
MYSQL_RIS_PASSWORD=R!suser>S3cured
MYSQL_RIS_HOST=10.10.123.141
MYSQL_RIS_PORT=3306
MYSQL_RIS_DATABASE=fk_radiology
MYSQL_RIS_TIMEOUT=5s
# Server Config Update
# Tambahkan explicit enable untuk REST
SERVER_REST_ENABLED=true
SERVER_REST_PORT=8096
# Kita matikan GRPC untuk sementara atau set portnya jika diperlukan
SERVER_GRPC_ENABLED=true
SERVER_GRPC_PORT=8097
# Redis
# The host 'redis' points to the Redis service in docker-compose.dev.yml
CACHE_REDIS_HOST=redis
CACHE_REDIS_PORT=6379
REDIS_PASSWORD=zahw@2904 # Please change this!
# External Services API Keys (Fill these with your actual secrets)
# BPJS Configuration
# BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
# BPJS_CONSID=5257
# BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482
# BPJS_SECRETKEY=1bV363512D
BPJS_ENABLED=true
BPJS_KDPPK=1323A036
// BPJS_SECRETKEY,BPJS_CONSID digunakan untuk VCLAIM, ANTROL, APLICARE, ICARE, DAN IHS
BPJS_CONSID=16041
BPJS_SECRETKEY=8pP3D5431E
BPJS_BASEURL=https://apijkn-dev.bpjs-kesehatan.go.id
BPJS_SERVICE_NAME=vclaim-rest-dev
// BPJS_SECRETKEY, BPJS_APOTEK_SECRETKEY hanya di gunakan untuk APOTEK AJA
BPJS_APOTEK_CONSID=16061
BPJS_APOTEK_SECRETKEY=5dP41F7EAE
untuk vclaim
BPJS_VCLAIM_USERKEY=467a2817fde5070f66c9a41fbeb8dc2c
# Ini untuk Antrol
BPJS_ANTROL_USERKEY=04a742fc834e0d3dc5b18913c8d46b90
untuk IHS
BPJS_IHS_USERKEY=dc36cb7e2fb9acacad3a4bc5c7423530
untuk Apotek
BPJS_APOTEK_USERKEY=e4305670a51dcbd2b857e95c34ad588e
# <- Masukkan User Key khusus Antrean RS di sini
# SatuSehat Configuration
SATUSEHAT_ENABLED=true
SATUSEHAT_ORG_ID=100026555
SATUSEHAT_FASYAKES_ID=3573011
SATUSEHAT_CLIENT_ID=l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1
SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1
SATUSEHAT_CONSENT_URL=https://api-satusehat.dto.kemkes.go.id/consent/v1
SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2
# Swagger
SWAGGER_HOST=localhost:8096 # Use the mapped port for external access
# CORS Configuration untuk Nuxt 3
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3005,http://meninjar.dev.rssa.id:8093,http://localhost:8080,http://localhost:5173,http://localhost:5174,http://10.10.123.143:3001
CORS_ALLOW_ALL=false
APP_ENV=development
MINIO_ENDPOINT=http://10.10.123.8:9000
MINIO_ACCESSKEY=tiBiJQ88gd08rMjYl9Nw
MINIO_SECRETKEY=1YXkdicAJbizIJ4vIuEjQwT9kIesImByqTFyphvJ
MINIO_USESSL=false
MINIO_BUCKETNAME=dev-test
+138
View File
@@ -0,0 +1,138 @@
# ----------------------------------------------------
# GENERAL APPLICATION SETTINGS
# ----------------------------------------------------
# Set application environment: "development" or "production"
APP_ENV=development
# Set gin mode: "debug" or "release"
GIN_MODE=debug
# ----------------------------------------------------
# SERVER CONFIGURATION
# ----------------------------------------------------
# Enable/disable REST and gRPC servers
SERVER_REST_ENABLED=true
SERVER_GRPC_ENABLED=false
# Ports for the servers
SERVER_REST_PORT=8094
SERVER_GRPC_PORT=50051
# Server timeouts in seconds
SERVER_READ_TIMEOUT=10
SERVER_WRITE_TIMEOUT=10
# ----------------------------------------------------
# DATABASE CONFIGURATION (Example for 'default' PostgreSQL)
# ----------------------------------------------------
# --- Primary 'default' Database ---
POSTGRES_DEFAULT_HOST=10.10.123.206
POSTGRES_DEFAULT_PORT=5432
POSTGRES_DEFAULT_USERNAME=postgres
POSTGRES_DEFAULT_PASSWORD=rss@j@y@2025
POSTGRES_DEFAULT_DATABASE=health
POSTGRES_DEFAULT_SCHEMA=public
POSTGRES_DEFAULT_SSLMODE=disable
# --- Example for another database connection, e.g., 'farmasi' ---
# SQLSERVER_FARMASI_HOST=192.168.1.100
# SQLSERVER_FARMASI_PORT=1433
# SQLSERVER_FARMASI_USERNAME=sa
# SQLSERVER_FARMASI_PASSWORD=your_password
# SQLSERVER_FARMASI_DATABASE=db_farmasi
# ----------------------------------------------------
# AUTHENTICATION
# ----------------------------------------------------
# Auth provider type: "jwt", "keycloak", "static", "hybrid"
AUTH_TYPE=jwt
# --- JWT Settings ---
# Secret key for signing internal JWT tokens. CHANGE THIS!
JWT_SECRET=your_very_secret_and_long_jwt_key
# --- Keycloak Settings ---
KEYCLOAK_ENABLED=false
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=myrealm
KEYCLOAK_CLIENT_ID=my-go-client
KEYCLOAK_CLIENT_SECRET=your_keycloak_client_secret
# These are for token validation
KEYCLOAK_ISSUER=http://localhost:8080/realms/myrealm
KEYCLOAK_AUDIENCE=my-go-client
KEYCLOAK_JWKS_URL=http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
# --- Static Token Settings (for simple machine-to-machine auth) ---
# AUTH_STATIC_TOKENS=token1,token2,token3
# ----------------------------------------------------
# CACHE CONFIGURATION (Redis)
# ----------------------------------------------------
CACHE_ENABLED=true
CACHE_REDIS_HOST=localhost
CACHE_REDIS_PORT=6379
REDIS_PASSWORD=
# Database for general cache
CACHE_REDIS_DB=1
# Database for rate limiting (can be the same or different)
RATE_LIMIT_REDIS_DB=0
# Cache TTLs
CACHE_DEFAULT_TTL=1h
CACHE_SESSION_TTL=24h
CACHE_RATE_LIMIT_TTL=1m
# ----------------------------------------------------
# SECURITY
# ----------------------------------------------------
# Comma-separated list of allowed origins for CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:8080,http://meninjar.dev.rssa.id:8094
# For development, you can allow all origins by setting this to true
CORS_ALLOW_ALL=false
# Rate Limiting
RATE_LIMIT_REQUESTS_PER_MINUTE=100
# ----------------------------------------------------
# BRIDGING SERVICES
# ----------------------------------------------------
# --- BPJS VClaim ---
# BPJS_ENABLED=true
# BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id
# BPJS_CONSID=5257
# BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482
# BPJS_SECRETKEY=1bV363512D
# BPJS_TIMEOUT=30s
BPJS_ENABLED=true
BPJS_CONSID=16041
BPJS_SECRETKEY=8pP3D5431E
BPJS_USERKEY=467a2817fde5070f66c9a41fbeb8dc2c
BPJS_BASEURL=https://apijkn-dev.bpjs-kesehatan.go.id
BPJS_SERVICE_NAME=vclaim-rest-dev
# --- SatuSehat ---
SATU_SEHAT_ENABLED=true
BRIDGING_SATUSEHAT_ORG_ID=100026555
BRIDGING_SATUSEHAT_FASYAKES_ID=3573011
BRIDGING_SATUSEHAT_CLIENT_ID=l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
BRIDGING_SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy
BRIDGING_SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1
BRIDGING_SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1
# ----------------------------------------------------
# SWAGGER DOCUMENTATION
# ----------------------------------------------------
SWAGGER_HOST=meninjar.dev.rssa.id:8094
SWAGGER_BASE_PATH=/api/v1
SWAGGER_SCHEMES=http,https
SWAGGER_TITLE=GoPrint Service API
SWAGGER_DESCRIPTION=API Documentation for GoPrint Service General
# ----------------------------------------------------
# LOGGER CONFIGURATION
# ----------------------------------------------------
# Log level: "debug", "info", "warn", "error"
LOGGER_LEVEL=debug
# Log format: "text" or "json"
LOGGER_FORMAT=text
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/service-general.iml" filepath="$PROJECT_DIR$/.idea/service-general.iml" />
</modules>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+647
View File
@@ -0,0 +1,647 @@
# 📚 GoPrint Service General - Project Documentation
## 📋 Deskripsi Proyek
Service-General adalah microservice berbasis Go yang menyediakan REST API dan gRPC untuk manajemen data person/pasien dengan integrasi ke sistem kesehatan Indonesia (BPJS & SatuSehat).
## 🏗️ Technical Architecture
Project ini mengimplementasikan **Clean Architecture** dengan **Domain-Driven Design (DDD)**:
- **Transport Layer**: REST API (Gin) + gRPC
- **Application Layer**: Business logic (UseCases)
- **Domain Layer**: Entities + Business rules
- **Infrastructure Layer**: Database, Cache, External services
### Multi-Database Support
Mendukung 5 jenis database sekaligus dengan connection pooling dan read replicas:
- PostgreSQL (Primary)
- MySQL
- SQL Server
- MongoDB
- SQLite
## 📁 Project Structure
```text
service-general/
├── cmd/api/main.go # Entry point aplikasi
├── internal/ # Business logic (Clean Architecture)
│ ├── person/ # Person/patient domain
│ │ ├── dto.go # Data Transfer Objects
│ │ ├── entity.go # Domain entities
│ │ ├── mapper.go # Object mappers
│ │ ├── repository.go # Repository interface & implementation
│ │ └── service.go # Business service
│ ├── auth/ # Authentication domain
│ ├── master/ # Master data domain
│ └── infrastructure/ # Infrastructure layer
│ ├── cache/ # Cache management
│ ├── config/ # Configuration
│ ├── database/ # Database connections
│ └── transport/ # HTTP & gRPC transport
├── pkg/ # Shared packages
│ ├── errors/ # Error handling system
│ ├── logger/ # Logging system
│ └── utils/query/ # Powerful query builder
├── scripts/ # Utility scripts
├── docs/ # Documentation
└── tools/ # Code generation tools
```
## 📦 Dependencies
### Core Dependencies
```
github.com/gin-gonic/gin v1.10.1 # REST API Framework
github.com/google/uuid v1.6.0 # UUID Generator
golang.org/x/crypto v0.44.0 # Cryptography
golang.org/x/sync v0.18.0 # Sync utilities
gorm.io/driver/postgres v1.5.11 # PostgreSQL Driver
```
### Database Drivers
```
gorm.io/driver/mysql v1.6.0 # MySQL Driver
gorm.io/driver/sqlite v1.6.0 # SQLite Driver
gorm.io/driver/sqlserver v1.6.3 # SQL Server Driver
go.mongodb.org/mongo-driver v1.17.6 # MongoDB Driver
github.com/jmoiron/sqlx v1.4.0 # SQL Extensions
```
### Utilities
```
github.com/go-playground/validator/v10 v10.27.0 # Input Validation
github.com/golang/protobuf v1.5.4 # Protocol Buffers
github.com/rs/zerolog v1.34.0 # Structured Logging
github.com/swaggo/gin-swagger v1.6.1 # Swagger Integration
google.golang.org/grpc v1.78.0 # gRPC Framework
```
## 🔗 API Endpoints
### REST API Base
- **Base URL**: `http://localhost:8080/api/v1`
- **Swagger UI**: `http://localhost:8080/swagger/index.html`
### Person Endpoints
- `GET /persons` - List persons with pagination
- `GET /persons/:id` - Get person detail
- `POST /persons` - Create new person
- `PUT /persons/:id` - Update person
- `DELETE /persons/:id` - Soft delete person
- `GET /persons/search` - Search with filters
### gRPC Services
- **Port**: 50051 (default)
- **Reflection**: Enabled untuk development
- **Proto Files**: Tersedia di `docs/api/`
## ⚙️ Environment Configuration
### Configuration in config.yaml
```yaml
server:
port: 8080
mode: debug
read_timeout: 10
write_timeout: 10
databases:
postgres:
type: postgres
host: 10.10.123.206
port: 5432
username: postgres
password: your_db_password
database: health
sslmode: disable
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
```
## 🗄️ Database Schema Overview
### Person Entity (Core)
```go
type Person struct {
Id int64 // Primary key
Name string // Nama lengkap
BirthDate *time.Time // Tanggal lahir
BirthRegency_Code *string // Kode kabupaten (6 digit)
Gender_Code *string // L/P (Laki-laki/Perempuan)
ResidentIdentityNumber *string // NIK (16 digit)
Religion_Code *string // Kode agama
Education_Code *string // Kode pendidikan
Occupation_Code *string // Kode pekerjaan
MaritalStatus_Code *string // Kode status marital
}
```
### Related Entities
- **PersonAddress**: Multiple addresses per person
- **PersonContact**: Contact information (email, phone)
- **PersonInsurance**: Insurance company data
- **PersonRelative**: Family/relative information
## 📋 Business Rules & Validation
### Person Validation Rules
```go
Name string `json:"name" binding:"required,min=2"`
BirthRegency_Code string `json:"birth_regency_code" validate:"omitempty,len=6"`
Gender_Code string `json:"gender_code" validate:"omitempty,oneof=L P"`
ResidentIdentityNumber string `json:"resident_identity_number" validate:"omitempty,len=16"`
Village_Code string `json:"village_code" validate:"required,len=10"`
```
### Key Validation Rules:
- **Name**: Required, minimum 2 characters
- **NIK**: 16 digits (optional)
- **Gender**: L/P only (Laki-laki/Perempuan)
- **Birth Regency**: 6 digits code
- **Village Code**: 10 digits code (for addresses)
- **Email**: Valid email format (for contacts)
## 🔗 Integration Points
### BPJS Integration
```yaml
bpjs:
base_url: https://apijkn.bpjs-kesehatan.go.id
cons_id: 5257
user_key: your_bpjs_user_key
secret_key: your_bpjs_secret_key
timeout: 30s
```
### SatuSehat Integration
```yaml
satu_sehat:
org_id: your_orgid
fasyakes_id: your_fasyakes_id
client_id: your_client_id
client_secret: your_client_secret
auth_url: https://api-satusehat.kemkes.go.id/oauth2/v1
base_url: https://api-satusehat.kemkes.go.id/fhir-r4/v1
```
## 🛠️ Development Commands
### Setup & Run
```bash
# Install dependencies
go mod download
# Run development server
make run
# atau
go run cmd/api/main.go
```
### Build & Test
```bash
# Build binary
make build
# atau
./scripts/build.sh
# Run tests
make test
# atau
go test ./...
# Database migration
./scripts/migrate.sh
# Generate proto files
./scripts/generate_proto.sh
```
## 🔐 Security Features
- JWT Authentication dengan multiple providers
- Rate limiting dengan Redis
- CORS protection
- Input validation & sanitization
- SQL injection prevention
- Audit trail untuk semua perubahan data
## 📊 Performance Features
- Connection pooling untuk database
- Query optimization dengan index
- Caching strategy (Redis)
- Read replicas support
- Soft delete untuk data retention
## 🧪 Testing Strategy
- Unit testing untuk business logic
- Integration testing untuk API
- External service mocking
- Load testing capabilities
## 🚀 Deployment Options
- Docker support (dev & prod)
- Environment-based configuration
- Health checks & monitoring
- Graceful shutdown
- Multi-stage build
## 📚 Documentation
- Swagger/OpenAPI documentation
- gRPC service definitions
- Code comments & examples
- Postman collections
- Architecture diagrams
## 🔮 Future Enhancements
- Event sourcing untuk audit trail
- CQRS implementation
- Multi-tenant support
- Advanced analytics
- AI/ML integration untuk data analytics
---
**Generated on**: $(date)
**Version**: 1.0.0
**Go Version**: $(go version | cut -d' ' -f3)
**Last Updated**: $(date +%Y-%m-%d)
```
Sekarang saya juga akan update script context.sh untuk membuatnya lebih lengkap:
```bash:/home/meninjar/goprint/service-general/scripts/context.sh
#!/bin/bash
# Script untuk generate project context secara lengkap
# Usage: ./scripts/context.sh [options]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
OUTPUT_FILE="$PROJECT_DIR/PROJECT_CONTEXT.md"
echo "🚀 Generating Service-General Project Context..."
# Fungsi untuk generate header
generate_header() {
cat << 'EOF'
# SERVICE-GENERAL PROJECT CONTEXT
## 📋 Project Overview
Service-General adalah microservice berbasis Go yang menyediakan REST API dan gRPC untuk manajemen data person/pasien dengan integrasi ke sistem kesehatan Indonesia (BPJS & SatuSehat).
## 🎯 Business Domain
- **Patient Management**: CRUD lengkap untuk data pasien
- **Health Insurance**: Integrasi dengan BPJS Kesehatan
- **National Health Platform**: Integrasi dengan SatuSehat (FHIR R4)
- **Master Data**: Manajemen data master (agama, pendidikan, pekerjaan, dll)
## 🏗️ Technical Architecture
### Clean Architecture + Domain-Driven Design
- **Transport Layer**: REST API (Gin) + gRPC
- **Application Layer**: Business logic (UseCases)
- **Domain Layer**: Entities + Business rules
- **Infrastructure Layer**: Database, Cache, External services
### Multi-Database Support
- PostgreSQL (primary)
- MySQL
- SQL Server
- MongoDB
- SQLite
EOF
}
# Fungsi untuk generate struktur directory
generate_directory_structure() {
echo "## 📁 Project Structure"
echo '```'
if command -v tree >/dev/null 2>&1; then
tree -L 3 -I 'tmp|vendor|.git|node_modules|*.log' "$PROJECT_DIR" 2>/dev/null | head -50
else
find "$PROJECT_DIR" -type d -not -path "*/tmp/*" -not -path "*/.git/*" | head -30
fi
echo '```'
}
# Fungsi untuk generate dependencies list
generate_dependencies() {
echo "## 📦 Dependencies"
echo "### Core Dependencies"
grep -E '^[[:space:]]*github.com/' "$PROJECT_DIR/go.mod" | head -10
echo ""
echo "### Database Drivers"
grep -E 'postgres|mysql|mongo|sqlite|sqlserver' "$PROJECT_DIR/go.mod"
echo ""
echo "### Security & Validation"
grep -E 'validator|crypto|jwt|auth' "$PROJECT_DIR/go.mod"
}
# Fungsi untuk generate business rules dari DTO
generate_business_rules() {
echo "## 📋 Business Rules & Validation"
if [ -f "$PROJECT_DIR/internal/person/dto.go" ]; then
echo "### Person Validation Rules"
echo '```go'
grep -E "validate:|binding:" "$PROJECT_DIR/internal/person/dto.go" | head -15
echo '```'
echo ""
echo "### Key Validation Rules:"
echo "- **Name**: Required, minimum 2 characters"
echo "- **NIK**: 16 digits (optional)"
echo "- **Gender**: L/P only (Laki-laki/Perempuan)"
echo "- **Birth Regency**: 6 digits code"
echo "- **Village Code**: 10 digits code (for addresses)"
echo "- **Email**: Valid email format (for contacts)"
fi
}
# Fungsi untuk generate database schema overview
generate_database_schema() {
echo "## 🗄️ Database Schema Overview"
if [ -f "$PROJECT_DIR/internal/person/entity.go" ]; then
echo "### Person Entity (Core)"
echo '```go'
# Ambil struct definition
sed -n '/type Person struct {/,/^}/p' "$PROJECT_DIR/internal/person/entity.go" | head -20
echo '```'
echo ""
echo "### Related Entities:"
echo "- **PersonAddress**: Multiple addresses per person"
echo "- **PersonContact**: Contact information (email, phone)"
echo "- **PersonInsurance**: Insurance company data"
echo "- **PersonRelative**: Family/relative information"
fi
}
# Fungsi untuk generate integration points
generate_integrations() {
echo "## 🔗 Integration Points"
if [ -f "$PROJECT_DIR/config.yaml" ]; then
echo "### BPJS Integration"
echo '```yaml'
grep -A 6 "bpjs:" "$PROJECT_DIR/config.yaml" 2>/dev/null || echo "BPJS configuration not found"
echo '```'
echo ""
echo "### SatuSehat Integration"
echo '```yaml'
grep -A 8 "satu_sehat:" "$PROJECT_DIR/config.yaml" 2>/dev/null || echo "SatuSehat configuration not found"
echo '```'
fi
}
# Fungsi untuk generate API endpoints dari code analysis
generate_api_endpoints() {
echo "## 🔗 API Endpoints"
echo "### REST API Base"
echo "- **Base URL**: \`http://localhost:8080/api/v1\`"
echo "- **Swagger UI**: \`http://localhost:8080/swagger/index.html\`"
echo ""
echo "### Person Endpoints"
echo "- \`GET /persons\` - List persons with pagination"
echo "- \`GET /persons/:id\` - Get person detail"
echo "- \`POST /persons\` - Create new person"
echo "- \`PUT /persons/:id\` - Update person"
echo "- \`DELETE /persons/:id\` - Soft delete person"
echo "- \`GET /persons/search\` - Search with filters"
echo ""
echo "### Auth Endpoints"
echo "- \`POST /auth/login\` - User login"
echo "- \`POST /auth/refresh\` - Refresh token"
echo "- \`POST /auth/logout\` - User logout"
echo ""
echo "### gRPC Services"
echo "- **Port**: 50051 (default)"
echo "- **Reflection**: Enabled untuk development"
echo "- **Proto Files**: Tersedia di \`docs/api/\`"
}
# Fungsi untuk generate environment configuration
generate_env_config() {
echo "## ⚙️ Environment Configuration"
if [ -f "$PROJECT_DIR/.env.example" ]; then
echo "### Environment Variables (.env.example)"
echo '```bash'
cat "$PROJECT_DIR/.env.example"
echo '```'
else
echo "### Configuration in config.yaml"
echo '```yaml'
grep -A 10 "server:" "$PROJECT_DIR/config.yaml" 2>/dev/null || echo "Server config not found"
echo '```'
fi
}
# Fungsi untuk generate development commands
generate_dev_commands() {
echo "## 🛠️ Development Commands"
echo "### Setup & Run"
echo '```bash'
echo "# Install dependencies"
echo "go mod download"
echo ""
echo "# Run development server"
echo "make run"
echo "# atau"
echo "go run cmd/api/main.go"
echo '```'
echo ""
echo "### Build & Test"
echo '```bash'
echo "# Build binary"
echo "make build"
echo "# atau"
echo "./scripts/build.sh"
echo ""
echo "# Run tests"
echo "make test"
echo "# atau"
echo "go test ./..."
echo ""
echo "# Database migration"
echo "./scripts/migrate.sh"
echo ""
echo "# Generate proto files"
echo "./scripts/generate_proto.sh"
echo '```'
}
# Fungsi untuk generate security features
generate_security_features() {
echo "## 🔐 Security Features"
echo "- JWT Authentication dengan multiple providers (JWT, Keycloak, Static)"
echo "- Rate limiting dengan Redis (default: 60 requests/minute)"
echo "- CORS protection dengan configurable origins"
echo "- Input validation & sanitization menggunakan go-playground/validator"
echo "- SQL injection prevention dengan parameterized queries"
echo "- Audit trail untuk semua perubahan data"
echo "- Soft delete untuk data retention"
echo "- Security headers (XSS, CSRF protection)"
}
# Fungsi untuk generate performance features
generate_performance_features() {
echo "## 📊 Performance Features"
echo "- Connection pooling untuk database (max 25 connections)"
echo "- Query optimization dengan proper indexing"
echo "- Caching strategy menggunakan Redis"
echo "- Read replicas support untuk load balancing"
echo "- Soft delete untuk mempertahankan performa query"
echo "- Pagination untuk semua list endpoints"
echo "- Timeout configuration untuk external services (30s default)"
}
# Fungsi untuk generate testing strategy
generate_testing_strategy() {
echo "## 🧪 Testing Strategy"
echo "- Unit testing untuk business logic di setiap domain"
echo "- Integration testing untuk API endpoints"
echo "- External service mocking untuk BPJS dan SatuSehat"
echo "- Load testing capabilities untuk performance validation"
echo "- Test coverage target: >80%"
}
# Fungsi untuk generate deployment info
generate_deployment_info() {
echo "## 🚀 Deployment Options"
echo "### Docker Support"
echo "- **Development**: \`docker-compose -f docker-compose.dev.yml up\`"
echo "- **Production**: \`docker-compose -f docker-compose.prod.yml up\`"
echo ""
echo "### Environment Variables"
echo "- Database configuration"
echo "- JWT secret dan auth settings"
echo "- External service credentials (BPJS, SatuSehat)"
echo "- Rate limiting configuration"
echo "- Logging level dan format"
}
# Generate complete context
generate_complete_context() {
generate_header > "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_directory_structure >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_dependencies >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_api_endpoints >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_env_config >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_database_schema >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_business_rules >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_integrations >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_dev_commands >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_security_features >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_performance_features >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_testing_strategy >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_deployment_info >> "$OUTPUT_FILE"
# Tambahkan closing section
cat >> "$OUTPUT_FILE" << EOF
## 📚 Additional Documentation
- **API Documentation**: Swagger/OpenAPI di \`/swagger/index.html\`
- **gRPC Documentation**: Tersedia di \`docs/api/grpc_docs.md\`
- **Postman Collection**: \`person.postman_collection.json\`
- **Architecture Diagrams**: Implementasi Clean Architecture + DDD
## 🔮 Future Enhancements
- Event sourcing untuk audit trail yang lebih baik
- CQRS (Command Query Responsibility Segregation) implementation
- Multi-tenant support untuk beberapa organisasi
- Advanced analytics dan reporting
- AI/ML integration untuk data analytics dan prediksi
- Service mesh integration untuk microservices
- Advanced caching strategies (Redis Cluster)
## 📊 Project Metrics
- **Go Version**: $(go version 2>/dev/null || echo "Go not installed")
- **Module**: service-general
- **Architecture**: Clean Architecture + DDD
- **API Types**: REST + gRPC
- **Databases**: 5 types (PostgreSQL, MySQL, SQL Server, MongoDB, SQLite)
- **External Integrations**: BPJS Kesehatan, SatuSehat Platform
- **Security**: JWT, Rate Limiting, CORS, Input Validation
---
**Generated on**: $(date)
**Version**: 1.0.0
**Last Updated**: $(date +%Y-%m-%d)
**Generator**: Service-General Context Script
EOF
echo "✅ Project context generated successfully!"
echo "📄 File created: $OUTPUT_FILE"
echo "📊 Context includes:"
echo " ✅ Project structure and architecture"
echo " ✅ Dependencies and technology stack"
echo " ✅ API endpoints and business rules"
echo " ✅ Database schema overview"
echo " ✅ Integration points (BPJS & SatuSehat)"
echo " ✅ Development commands and deployment"
echo " ✅ Security and performance features"
echo " ✅ Testing strategy and future enhancements"
}
# Main execution
main() {
cd "$PROJECT_DIR"
case "${1:-}" in
--help|-h)
echo "Usage: $0 [options]"
echo "Options:"
echo " --help, -h Show this help message"
echo " --generate Generate complete project context (default)"
echo " --structure Show directory structure only"
echo " --deps Show dependencies only"
echo " --api Show API endpoints only"
echo " --rules Show business rules only"
echo " --security Show security features only"
echo " --performance Show performance features only"
;;
--generate)
generate_complete_context
;;
--structure)
generate_directory_structure
;;
--deps)
generate_dependencies
;;
--api)
generate_api_endpoints
;;
--rules)
generate_business_rules
;;
--security)
generate_security_features
;;
--performance)
generate_performance_features
;;
*)
generate_complete_context
;;
esac
}
# Run main function
main "$@"
echo "✨ Context generation completed!"
```
+40
View File
@@ -0,0 +1,40 @@
# Multi-stage build yang lebih optimal
FROM golang:1.22-alpine AS build
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /build
# Cache go mod dependencies
COPY go.mod go.sum ./
RUN go mod download && go mod verify
# Copy source code
COPY . .
# Build dengan optimasi
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -a -installsuffix cgo \
-ldflags='-w -s -extldflags "-static"' \
-o main cmd/api/main.go
# Final stage - distroless untuk keamanan
FROM gcr.io/distroless/static:nonroot
# Copy timezone data
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=build /build/main /app/main
# Use non-root user
USER nonroot:nonroot
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/main", "-health"] || exit 1
EXPOSE 8080
ENTRYPOINT ["/app/main"]
+20
View File
@@ -0,0 +1,20 @@
# Versi development dengan hot reload
FROM golang:1.25-alpine AS dev
WORKDIR /app
# Install air untuk hot reload
RUN go install github.com/cosmtrek/[email protected]
# Install dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Expose port
EXPOSE 8094
# Run air untuk hot reload
CMD ["air", "-c", ".air.toml"]
+203
View File
@@ -0,0 +1,203 @@
.PHONY: dev prod clean
dev:
docker-compose -f docker-compose.dev.yml up --build
logs:
docker-compose -f docker-compose.dev.yml logs -f
prod:
docker-compose -f docker-compose.prod.yml up --build
clean:
docker-compose -f docker-compose.dev.yml down -v
docker-compose -f docker-compose.prod.yml down -v
security-check:
@echo "🔍 Running Static Application Security Testing (SAST)..."
gosec -quiet ./...
audit:
@echo "🔍 Checking for known vulnerabilities in dependencies..."
govulncheck ./...
help:
@echo "Available targets:"
@echo " proto - Generate all protobuf files"
@echo " proto-all - Generate all protobuf files with cleanup"
@echo " generate-proto - Generate specific proto file (usage: make generate-proto FILE=path/to/file.proto)"
@echo " generate-ethnic-grpc - Generate ethnic gRPC service"
@echo " generate-person-grpc - Generate person gRPC service"
@echo " clean - Clean generated files"
@echo " build - Build the application"
@echo " run - Run the application"
@echo " test - Run tests"
@echo " test-coverage - Run tests and generate coverage report"
@echo " govulncheck - Check for vulnerabilities"
@echo " seeder-build - Build the seeder CLI tool"
@echo " seeder-list - List all available tables for seeding"
@echo " seeder-seed - Seed specific table (usage: make seeder-seed TABLE=province)"
@echo " seeder-seed-all - Seed all tables"
@echo " seeder-dry-run - Dry run seeding for table (usage: make seeder-dry-run TABLE=province)"
@echo " seeder-validate - Validate CSV files and configuration"
# Generate specific proto file dengan penanganan error
generate-proto:
@if [ -z "$(FILE)" ]; then \
echo "❌ Usage: make generate-proto FILE=path/to/file.proto"; \
echo "Example: make generate-proto FILE=internal/infrastructure/transport/grpc/proto/v1/ethnic.proto"; \
exit 1; \
fi
@echo "🚀 Generating protobuf code untuk: $(FILE)"
@if command -v protoc-gen-validate &> /dev/null; then \
echo "✅ protoc-gen-validate ditemukan, generate dengan validasi..."; \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:." \
$(FILE); \
else \
echo "⚠️ protoc-gen-validate tidak ditemukan, generate tanpa validasi..."; \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
$(FILE); \
fi
# Seeder targets
seeder-build:
@echo "🔨 Building seeder CLI tool..."
@go build -o bin/seeder cmd/seeder/main.go
@echo "✅ Seeder CLI built successfully at bin/seeder"
seeder-list: seeder-build
@echo "📋 Listing all available tables for seeding..."
@./bin/seeder list
seeder-seed: seeder-build
@if [ -z "$(TABLE)" ]; then \
echo "❌ Usage: make seeder-seed TABLE=<table-name>"; \
echo "Example: make seeder-seed TABLE=province"; \
echo "Available tables:"; \
./bin/seeder list; \
exit 1; \
fi
@echo "🌱 Seeding table: $(TABLE)"
@./bin/seeder seed $(TABLE)
seeder-seed-all: seeder-build
@echo "🌱 Seeding all tables..."
@./bin/seeder seed all
seeder-dry-run: seeder-build
@if [ -z "$(TABLE)" ]; then \
echo "❌ Usage: make seeder-dry-run TABLE=<table-name>"; \
echo "Example: make seeder-dry-run TABLE=province"; \
exit 1; \
fi
@echo "🔍 Dry-run seeding for table: $(TABLE)"
@./bin/seeder dry-run $(TABLE)
seeder-validate: seeder-build
@echo "✅ Validating all tables and CSV files..."
@./bin/seeder validate all
# Advanced seeder with options
seeder-advanced: seeder-build
@if [ -z "$(TABLE)" ]; then \
echo "❌ Usage: make seeder-advanced TABLE=<table-name> [OPTIONS]"; \
echo "Options:"; \
echo " BATCH_SIZE=<n> - Set batch size"; \
echo " DELETE_BEFORE=1 - Delete existing data before seeding"; \
echo " CSV_PATH=<path> - Override CSV file path"; \
exit 1; \
fi
@echo "🌱 Advanced seeding for table: $(TABLE)"
@./bin/seeder seed $(TABLE) -batch-size=$(or $(BATCH_SIZE),100) $(if $(DELETE_BEFORE),-delete-before,) $(if $(CSV_PATH),-csv-path=$(CSV_PATH),)
clean-seeder:
@echo "🧹 Cleaning seeder binary..."
@rm -f bin/seeder
@echo "✅ Seeder binary cleaned"
@echo "✅ Proto code berhasil digenerate"
# Generate all protobuf files
proto:
@echo "Generating all protobuf files..."
@for proto in internal/infrastructure/transport/grpc/proto/v1/*.proto; do \
echo "Processing $$proto..."; \
if command -v protoc-gen-validate &> /dev/null; then \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:." \
$$proto; \
else \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
$$proto; \
fi; \
done
@echo "✅ All protobuf files generated"
# Generate all protobuf files with cleanup
proto-all: clean-gen proto
# Generate specific service (will be added by generator script)
generate-ethnic-grpc:
@echo "Generating ethnic gRPC service..."
@if [ -f "internal/infrastructure/transport/grpc/proto/v1/ethnic.proto" ]; then \
if command -v protoc-gen-validate &> /dev/null; then \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:." \
internal/infrastructure/transport/grpc/proto/v1/ethnic.proto; \
else \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
internal/infrastructure/transport/grpc/proto/v1/ethnic.proto; \
fi; \
else \
echo "⚠️ File ethnic.proto tidak ditemukan, skip..."; \
fi
@echo "✅ Ethnic gRPC service generated"
generate-person-grpc:
@echo "Generating person gRPC service..."
@if command -v protoc-gen-validate &> /dev/null; then \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:." \
internal/infrastructure/transport/grpc/proto/v1/person.proto; \
else \
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
internal/infrastructure/transport/grpc/proto/v1/person.proto; \
fi
@echo "✅ Person gRPC service generated"
# Clean generated files
clean-gen:
@echo "Cleaning generated files..."
@rm -rf internal/infrastructure/transport/grpc/gen/
@mkdir -p internal/infrastructure/transport/grpc/gen
@echo "✅ Generated files cleaned"
# Generate village gRPC service
generate-village-grpc:
@echo "Generating village gRPC service..."
@protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--validate_out="lang=go:." \
--experimental_allow_proto3_optional \
internal/infrastructure/transport/grpc/proto/v1/village.proto
@echo "✅ village gRPC service generated"
.PHONY: test test-coverage
# Run tests
test:
@echo "🧪 Running tests..."
@go test -v ./...
# Run tests with coverage
test-coverage:
@echo "🧪 Running tests with coverage..."
@go test -v -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
@echo "✅ Coverage report generated at coverage.html"
+589
View File
@@ -0,0 +1,589 @@
# GoPrint Service General API
A comprehensive Go microservice providing REST API and gRPC support. Built with **Clean Architecture**, **Domain-Driven Design (DDD)**, and **CQRS Pattern**, featuring multi-database support, intelligent caching, and direct integration with Indonesian Health Systems (BPJS VClaim & SatuSehat).
## 🏗️ Architecture
This project strongly follows **Clean Architecture** principles, **CQRS Pattern** (Command and Query Responsibility Segregation), and **Domain-Driven Design (DDD)** patterns:
```
┌─────────────────────────────────────────────────────────────┐
│ Transport Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ REST API (Gin) │ │ gRPC API │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Auth Service │ │ Role/Master Service │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Command Repository │ │ Query Repository │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Multi-DB & Cache │ │ BPJS VClaim & SatuSehat │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## 🚀 Features
### Core Features
-**Dual API Support**: REST (Gin) + gRPC endpoints
-**Clean Architecture**: Domain → Application → Infrastructure → Transport
-**Domain-Driven Design**: Bounded Contexts (Auth, Patient)
-**Multi-Database Support**: PostgreSQL, MySQL, SQL Server, MongoDB, SQLite
-**Connection Pooling**: Optimized database connections with health monitoring
-**Read Replicas**: Load balancing with round-robin strategy
-**Audit Trail**: Complete audit logging for data changes
-**Soft Delete**: Data retention with active/inactive views
### Security Features
-**JWT Authentication**: Multiple auth providers (JWT, Keycloak, Static)
-**CORS Support**: Configurable cross-origin resource sharing
-**Rate Limiting**: Configurable request rate limiting
-**Security Headers**: XSS, CSRF, and other security headers
-**Input Validation**: Comprehensive data validation
-**SQL Injection Prevention**: Parameterized queries and input sanitization
### Integration Features
-**BPJS Integration**: Indonesian health insurance system integration
-**SatuSehat Integration**: Indonesian health data platform
-**Swagger Documentation**: Auto-generated API documentation
-**Health Checks**: Database and service health monitoring
-**Metrics Collection**: Application performance metrics
### Development Features
-**Hot Reload**: Development server with auto-restart
-**Code Generation**: Automated entity and service generation
-**Docker Support**: Containerized deployment
-**Environment Configuration**: YAML + Environment variables
-**Structured Logging**: Comprehensive logging system
## 📁 Project Structure
```text
service-general/ # 🏠 Root Project
├── cmd/api/ # 🚀 Application Entry Point (main.go)
├── internal/ # 🔒 Private Application Code
│ ├── auth/ # 🔐 Authentication Context
│ ├── master/ # 📊 Master Data Context
│ ├── infrastructure/ # 🏗️ Technical Infrastructure (DB, HTTP, gRPC)
│ └── interfaces/ # 🔌 External Services (BPJS, SatuSehat)
├── pkg/ # 📦 Public Reusable Libraries
│ ├── errors/ # 🚨 Error handling system
│ ├── logger/ # 📝 Logging system
│ └── utils/query/ # 🔍 Advanced Query Builder
├── scripts/ # 🔄 Automation Scripts
├── tools/ # 🔧 Development Tools
├── docs/ # 📚 Documentation
├── config.yaml # ⚙️ Configuration file
└── Makefile # 🔨 Build automation
```
## Requirements
- Go 1.21+
- PostgreSQL 12+ (recommended)
- Redis 6+ (for rate limiting)
- Docker & Docker Compose (optional)
## 🛠️ Installation
### 1. Clone the Repository
```bash
git clone <repository-url>
cd service-general
```
### 2. Install Dependencies
```bash
go mod download
go mod tidy
```
### 3. Install Development Tools
```bash
# Install gRPC code generation tools
go install github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
go install github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
go install google.golang.org/protobuf/cmd/[email protected]
# Install Swagger generation tool
go install github.com/swaggo/swag/cmd/[email protected]
```
### 4. Setup Database
```bash
# Create PostgreSQL database
createdb api_service
# Run migrations
psql -d api_service -f migrations/001_initial_schema.sql
```
### 5. Configure Environment
```bash
# Copy configuration template
cp config.yaml.example config.yaml
# Edit configuration
vim config.yaml
```
### 6. Run the Application
```bash
# Development mode
make run
# Or manually
go run cmd/api/main.go
```
## 🔧 Configuration
### Environment Variables
```bash
# Server Configuration
PORT=8080
GIN_MODE=debug
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=password
DB_DATABASE=api_service
# JWT Configuration
JWT_SECRET=your-secret-key
# BPJS Configuration
BPJS_CONSID=your-cons-id
BPJS_SECRETKEY=your-secret-key
BPJS_USERKEY=your-user-key
# Rate Limiting
RATE_LIMIT_REQUESTS_PER_MINUTE=60
```
### Configuration File (config.yaml)
```yaml
server:
rest:
enabled: true
port: 8080
grpc:
enabled: true
port: 50051
mode: debug
read_timeout: 30
write_timeout: 30
databases:
primary:
type: postgres
host: localhost
port: 5432
username: postgres
password: password
database: api_service
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
auth:
type: jwt
static_tokens: []
bpjs:
base_url: https://apijkn.bpjs-kesehatan.go.id
cons_id: ""
secret_key: ""
user_key: ""
timeout: 30s
security:
trusted_origins:
- http://localhost:3000
- http://localhost:8080
max_input_length: 500
rate_limit:
requests_per_minute: 60
```
## 📚 API Documentation
### REST API Endpoints
#### Authentication
- `POST /api/v1/auth/login` - User login
- `POST /api/v1/auth/logout` - User logout
- `POST /api/v1/auth/refresh` - Refresh token
- `GET /api/v1/auth/profile` - Get user profile
#### Patients
- `GET /api/v1/patients` - List patients (paginated)
- `GET /api/v1/patients/{id}` - Get patient by ID
- `POST /api/v1/patients` - Create new patient
- `PUT /api/v1/patients/{id}` - Update patient
- `DELETE /api/v1/patients/{id}` - Delete patient
#### Medical Records
- `GET /api/v1/patients/{id}/medical-records` - Get patient medical records
- `POST /api/v1/patients/{id}/medical-records` - Create medical record
- `PUT /api/v1/medical-records/{id}` - Update medical record
#### Health Check
- `GET /api/v1/health` - Application health status
- `GET /api/v1/health/database` - Database connection status
### gRPC API Endpoints
#### Auth Service
```protobuf
service AuthService {
rpc Login(LoginRequest) returns (LoginResponse);
rpc Logout(LogoutRequest) returns (LogoutResponse);
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse);
}
```
#### Patient Service
```protobuf
service PatientService {
rpc ListPatients(ListPatientsRequest) returns (ListPatientsResponse);
rpc GetPatient(GetPatientRequest) returns (GetPatientResponse);
rpc CreatePatient(CreatePatientRequest) returns (CreatePatientResponse);
rpc UpdatePatient(UpdatePatientRequest) returns (UpdatePatientResponse);
rpc DeletePatient(DeletePatientRequest) returns (DeletePatientResponse);
}
```
## 🐳 Docker Deployment
### Build Docker Image
```bash
docker build -t service-general .
```
### Run with Docker Compose
```bash
docker-compose up -d
```
### Docker Compose Configuration
```yaml
version: '3.8'
services:
service-general:
build: .
ports:
- "8080:8080"
- "50051:50051"
environment:
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
postgres:
image: postgres:15
environment:
POSTGRES_DB: api_service
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
```
## 🧪 Testing
### Run Tests
```bash
# Run all tests
make test
# Run tests with coverage
make test-coverage
# Run integration tests
make test-integration
```
### Test Examples
```bash
# Test health endpoint
curl http://localhost:8080/api/v1/health
# Test patient creation
curl -X POST http://localhost:8080/api/v1/patients \
-H "Content-Type: application/json" \
-d '{
"nik": "1234567890123456",
"name": "John Doe",
"date_of_birth": "1990-01-01",
"gender": "L"
}'
```
## 📊 Monitoring & Metrics
### Health Check Endpoints
- `/api/v1/health` - Overall application health
- `/api/v1/health/database` - Database connection status
- `/api/v1/health/external` - External service status
### Metrics Collection
- Request count and duration
- Database connection pool metrics
- Error rates and types
- Authentication success/failure rates
### Logging
- Structured JSON logging
- Request/Response logging
- Error stack traces
- Performance metrics
## 🔒 Security
### Authentication
- JWT tokens with configurable expiration
- Multiple auth providers (JWT, Keycloak, Static)
- Token refresh mechanism
- Session management
### Authorization
- Role-based access control (RBAC)
- Permission-based endpoints
- API key authentication
- Rate limiting per user/IP
### Data Protection
- SQL injection prevention
- XSS protection headers
- CSRF protection
- Input validation and sanitization
## 🚀 Performance
### Database Optimization
- Connection pooling with configurable limits
- Read replica support with load balancing
- Query optimization and indexing
- Health monitoring and automatic reconnection
### Caching
- Redis-based session storage
- Application-level caching
- Database query result caching
- Static asset caching
### Scalability
- Horizontal scaling support
- Load balancer ready
- Microservice architecture
- Container orchestration ready
## 🛠️ Development
### Code Generation
```bash
# Generate new entity
make generate entity=Patient
# Generate service for entity
make generate service=Patient
# Generate CRUD handlers
make generate crud=Patient
```
### Database Migrations
```bash
# Create new migration
make migration create=add_new_table
# Run migrations
make migration up
# Rollback migration
make migration down
# Create a new migration using goose
goose -dir internal/infrastructure/database/sql create create_users_table sql
```
### API Documentation
```bash
# Generate Swagger docs
make swagger
# Serve Swagger UI
make swagger-serve
```
## 📈 Production Deployment
### Environment Setup
```bash
# Production environment variables
export GIN_MODE=release
export PORT=8080
export DB_HOST=your-production-db
export JWT_SECRET=your-production-secret
```
### Monitoring Setup
- Prometheus metrics endpoint
- Grafana dashboard templates
- Log aggregation with ELK stack
- Alert configuration
### CI/CD Pipeline
- GitHub Actions workflow
- Automated testing
- Docker image building
- Deployment automation
## 🤝 Contributing
1. Fork the repository
2. Create feature branch (`git checkout -b feature/amazing-feature`)
3. Commit changes (`git commit -m 'Add amazing feature'`)
4. Push to branch (`git push origin feature/amazing-feature`)
5. Open Pull Request
### Code Style
- Follow Go conventions
- Use meaningful variable names
- Add comments for complex logic
- Write unit tests for new features
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🆘 Support
For support and questions:
- Create an issue on GitHub
- Check the [Wiki](wiki) for documentation
- Join our [Discord](https://discord.gg/your-server) community
## 🗺️ Roadmap
service-general/ # 🏠 Root Project
├── 📁 cmd/ # 🚀 Application Entry Points
│ └── 📁 api/ # API Application
│ └── 📄 main.go # 🎯 Main entry point (wires everything together)
├── 📁 internal/ # 🔒 Private Application Code
│ ├── 📁 auth/ # 🔐 Bounded Context: Authentication
│ │ ├── 📁 domain/ # 🏛️ Core auth entities & business rules
│ │ │ └── 📄 model.go # User, Role entities
│ │ ├── 📁 repository/ # 💾 Data access interfaces
│ │ │ └── 📄 repo.go # Auth repository interface
│ │ └── 📁 usecase/ # ⚙️ Application business logic
│ │ └── 📄 service.go # Auth services (login, register, etc)
│ │
│ ├── 📁 master/ # 📊 Bounded Context: Master Data
│ │ ├── 📄 dto.go # 📋 Data Transfer Objects
│ │ ├── 📄 entity.go # 🗃️ Master entities
│ │ ├── 📄 repository.go # 🏪 Master repository interface
│ │ └── 📄 service.go # 🛠️ Master business logic
│ │
│ ├── 📁 infrastructure/ # 🏗️ Technical Infrastructure Layer
│ │ ├── 📁 config/ # ⚙️ Configuration management
│ │ │ └── 📄 config.go # Load config.yaml
│ │ ├── 📁 container/ # 📦 Dependency injection container
│ │ ├── 📁 database/ # 💽 Database connections & migrations
│ │ │ ├── 📄 database.go # Multi-database support (PostgreSQL, MySQL, SQL Server, MongoDB)
│ │ │ └── 📁 migrations/ # 🗄️ Database migrations
│ │ └── 📁 transport/ # 🌐 API Transport Layer
│ │ ├── 📁 grpc/ # 🚀 gRPC implementation
│ │ │ ├── 📁 gen/ # 📝 Generated gRPC code
│ │ │ ├── 📁 proto/ # 📄 Protocol buffer definitions
│ │ │ └── 📁 servers/ # 🖥️ gRPC server implementations
│ │ └── 📁 http/ # 🌐 REST API implementation
│ │ ├── 📁 handlers/ # 🎯 HTTP request handlers
│ │ ├── 📁 middleware/ # 🛡️ HTTP middlewares
│ │ ├── 📁 routes/ # 🗺️ Route definitions
│ │ └── 📁 servers/ # 🖥️ HTTP server setup
│ │
│ └── 📁 interfaces/ # 🔌 External Service Interfaces
│ ├── 📁 bpjs/ # 🏥 BPJS Integration
│ │ └── 📁 vclaim/ # 💳 BPJS VClaim service
│ │ ├── 📄 VCLAIM.md # 📖 Documentation
│ │ ├── 📄 client.go # 🌐 HTTP client
│ │ ├── 📄 crypto.go # 🔐 Cryptography utilities
│ │ ├── 📄 service.go # 🛠️ VClaim service logic
│ │ └── 📄 types.go # 📋 Type definitions
│ └── 📁 satusehat/ # 🏥 SatuSehat Integration (HL7 FHIR)
├── 📁 pkg/ # 📦 Public Reusable Libraries
│ ├── 📁 errors/ # 🚨 Error handling system
│ │ ├── 📄 ERRORS.md # 📖 Error documentation
│ │ ├── 📄 builder.go # 🏗️ Error builder
│ │ ├── 📄 codes.go # 🔢 Error codes
│ │ ├── 📄 grpc.go # 🌐 gRPC error handling
│ │ ├── 📄 http.go # 🌐 HTTP error handling
│ │ └── 📄 validator.go # ✅ Validation errors
│ ├── 📁 logger/ # 📝 Logging system
│ │ └── 📄 logger.go # 🪵 Configurable logger
│ ├── 📁 metrics/ # 📊 Application metrics
│ ├── 📁 response/ # 📤 HTTP response utilities
│ │ └── 📄 response.go # 🎯 Standardized responses
│ └── 📁 utils/ # 🛠️ Utility functions
│ └── 📁 query/ # 🔍 Advanced Query Builder
│ ├── 📄 BUILDER.md # 📖 Query builder docs
│ ├── 📄 builder_query.go # 🏗️ Main query builder
│ ├── 📄 dialects.go # 🗣️ Database dialect support
│ ├── 📄 sql_builder.go # 🏗️ SQL query builder
│ ├── 📄 mongo_builder.go # 🍃 MongoDB query builder
│ └── 📄 sql_security.go # 🔒 SQL injection protection
├── 📁 scripts/ # 🔄 Automation Scripts
│ ├── 📄 build.sh # 🔨 Build application
│ ├── 📄 generate_proto.sh # 📝 Generate gRPC protobuf
│ ├── 📄 migrate.sh # 🗄️ Run database migrations
│ └── 📄 seed.sh # 🌱 Seed initial data
├── 📁 tools/ # 🔧 Development Tools
│ ├── 📄 generate.go # 🏭 Code generator
│ └── 📄 generate_config.yaml # ⚙️ Generator configuration
├── 📄 config.yaml # ⚙️ Main configuration file
├── 📄 go.mod # 📦 Go module dependencies
├── 📄 go.sum # 🔒 Dependency checksums
├── 📄 Makefile # 🔨 Build automation
├── 📄 README.md # 📖 Project documentation
├── 📄 Dockerfile # 🐳 Production container
├── 📄 Dockerfile.dev # 🐳 Development container
├── 📄 docker-compose.dev.yml # 🐳 Dev environment
├── 📄 docker-compose.prod.yml # 🐳 Production environment
└── 📄 .air.toml # 🔄 Live reload configuration
goose -dir internal/infrastructure/database/sql create create_users_table sql
Executable
BIN
View File
Binary file not shown.
+391
View File
@@ -0,0 +1,391 @@
// File: /home/meninjar/goprint/service-general/main.go
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"service/internal/auth"
antrol "service/internal/bpjs/antrol/reference"
aplicareBed "service/internal/bpjs/aplicare/bed"
apotekDpho "service/internal/bpjs/apotek/reference/dpho"
apotekPoli "service/internal/bpjs/apotek/reference/poli"
"service/internal/bpjs/vclaim/peserta"
"service/internal/bpjs/vclaim/sep"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
grpcHandlers "service/internal/infrastructure/transport/grpc/handlers"
httpServer "service/internal/infrastructure/transport/http/servers"
bpjsFactory "service/internal/interfaces/bpjs"
"service/internal/interfaces/minio"
satuSehatFactory "service/internal/interfaces/satusehat"
satuSehatAuth "service/internal/satusehat/reference/auth"
satuSehatKfa "service/internal/satusehat/reference/kfa"
satuSehatLocation "service/internal/satusehat/reference/location"
satuSehatOrganization "service/internal/satusehat/reference/organization"
satuSehatPatient "service/internal/satusehat/reference/patient"
satuSehatPractitioner "service/internal/satusehat/reference/practitioner"
satuSehatCondition "service/internal/satusehat/usecase/condition"
satuSehatEncounter "service/internal/satusehat/usecase/encounter"
satuSehatProcedure "service/internal/satusehat/usecase/procedure"
"service/pkg/logger"
imagingStudyWorker "service/internal/worker/satusehat/imagingstudy"
// roleComponent "service/internal/master/role/component"
roleMaster "service/internal/master/role/master"
rolePages "service/internal/master/role/pages"
rolePermission "service/internal/master/role/permission"
_ "service/docs/swagger" // Wajib: import swagger docs yang di-generate oleh swag CLI
grpcServers "service/internal/infrastructure/transport/grpc/servers"
"golang.org/x/sync/errgroup"
)
// @title GoPrint Service General API
// @version 1.0.0
// @description REST API for Service General including Multi-DB, BPJS, and SatuSehat integrations.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email [email protected]
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
// 1. Load Config
cfg := config.LoadConfig()
// 2. Init Logger dengan konfigurasi dari config
loggerConfig := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: "both", // Output ke console dan ke dalam folder daily logs
ServiceName: "service-general",
EnableCaller: cfg.Server.Mode != "production", // Aktifkan caller info di non-production
Environment: cfg.Server.Mode, // Gunakan mode server untuk environment
}
logger.Init(loggerConfig)
// Sekarang bisa menggunakan logger custom
logger.Default().Info("Application starting",
logger.String("service", "service-general"),
logger.String("environment", cfg.Server.Mode),
logger.String("log_level", cfg.Logger.Level),
)
// Di dalam main.go
// Asumsikan di config.yaml Anda ada database bernama "postgres" dan "mysql_legacy" contoh pemanggilan dan prosesing 2 tabel berbeda dalam satu repository
// reportRepo := report.NewRepository(dbService, "postgres", "mysql_legacy")
// reportService := report.NewService(reportRepo)
// reportService ini tinggal di-pass ke dalam Handler untuk dijadikan endpoint REST API.
// Add debugging to see what config was loaded
logger.Default().Debug("Config loaded",
logger.Any("database_count", len(cfg.Databases)),
logger.Any("cache_enabled", cfg.Cache.Enabled),
logger.Any("server_mode", cfg.Server.Mode),
)
if err := cfg.Validate(); err != nil {
logger.Default().Fatal("Config validation failed", logger.ErrorField(err))
}
// --- Inisialisasi Minio Object Storage ---
minio.Connect()
logger.Default().Info("Minio Object Storage client initialized")
// 3. Init Database Manager (Mendukung CQRS & Multi-DB)
dbService := database.New(cfg)
defer dbService.Close()
// Menjalankan Automigrasi
// if err := dbService.Migrate(); err != nil {
// logger.Default().Warn("Database migration completed with issues", logger.ErrorField(err))
// }
// Get Master/Primary GORM DB Connection untuk modul lama
// Gunakan nama koneksi "postgres" atau "default" sesuai config.yaml Anda
gormDB, err := dbService.GetGormDB("default")
if err != nil {
logger.Default().Fatal("Failed to get primary gorm database", logger.ErrorField(err))
}
// --- [NEW] Inisialisasi Message Broker (Kafka / RabbitMQ) untuk Event-Driven Microservice ---
// Jika bertindak sebagai gateway atau microservice independen, inisialisasi Kafka di sini
// kafkaProducer := broker.NewKafkaProducer(cfg.Kafka.Brokers)
// defer kafkaProducer.Close()
// logger.Default().Info("✅ Kafka Producer initialized")
// 4. Init Cache using factory pattern
cacheFactory := cache.NewFactory(cfg.Cache)
cacheManager, err := cacheFactory.CreateManager()
if err != nil {
logger.Default().Fatal("Failed to initialize cache manager", logger.ErrorField(err))
}
defer cacheManager.Close()
if cfg.Cache.Enabled {
logger.Default().Info("Cache initialized", logger.String("host", cfg.Cache.Redis.Host), logger.Int("port", cfg.Cache.Redis.Port))
} else {
logger.Default().Warn("Cache disabled, using NoOp cache")
}
// 5. Init Layers (CQRS Repositories & Services)
// Modul Auth
authCmdRepo := auth.NewCommandRepository(dbService, "default")
authQueryRepo := auth.NewQueryRepository(dbService, "default")
authSvc := auth.NewService(authCmdRepo, authQueryRepo, cacheManager, cfg)
// Inisialisasi Role Pages, Permission, dan Component (Menggunakan pola CQRS baru)
rolePagesCmdRepo := rolePages.NewCommandRepository(dbService, "default")
rolePagesQueryRepo := rolePages.NewQueryRepository(dbService, "default")
rolePagesService := rolePages.NewService(rolePagesCmdRepo, rolePagesQueryRepo, cacheManager)
rolePermissionCmdRepo := rolePermission.NewCommandRepository(dbService, "default")
rolePermissionQueryRepo := rolePermission.NewQueryRepository(dbService, "default")
rolePermissionService := rolePermission.NewService(rolePermissionCmdRepo, rolePermissionQueryRepo, rolePagesQueryRepo, cacheManager)
// Modul Role Master (Hasil Generator)
roleMasterCmdRepo := roleMaster.NewCommandRepository(dbService, "default")
roleMasterQueryRepo := roleMaster.NewQueryRepository(dbService, "default")
roleMasterService := roleMaster.NewService(roleMasterCmdRepo, roleMasterQueryRepo, cacheManager)
// roleComponentCmdRepo := roleComponent.NewCommandRepository(dbService, "postgres")
// roleComponentQueryRepo := roleComponent.NewQueryRepository(dbService, "postgres")
// roleComponentService := roleComponent.NewService(roleComponentCmdRepo, roleComponentQueryRepo)
// --- [NEW] Inisialisasi Modul CQRS Baru (Memanfaatkan DB Service) ---
// Contoh ketika mengimplementasikan entity "Province" atau "Person":
// provinceCmdRepo := province.NewCommandRepository(dbService, "default")
// provinceQueryRepo := province.NewQueryRepository(dbService, "default")
// provinceSvc := province.NewService(provinceCmdRepo, provinceQueryRepo)
// --- [NEW] Inisialisasi Factory BPJS ---
bpjsServices := bpjsFactory.NewBPJSFactory(cfg.Bpjs)
// 1. Modul VClaim (Lama)
vclaimServiceEngine := bpjsServices.VClaim()
sepRepo := sep.NewRepository(vclaimServiceEngine)
sepService := sep.NewService(sepRepo)
pesertaRepo := peserta.NewRepository(vclaimServiceEngine)
pesertaService := peserta.NewService(pesertaRepo)
// 2. Modul Antrean RS (Baru)
antreanClient := bpjsServices.AntreanRS()
antrolRepo := antrol.NewRepository(antreanClient)
antrolService := antrol.NewService(antrolRepo)
// 3. Modul Aplicares (Bed Ketersediaan Tempat Tidur)
aplicareClient := bpjsServices.Aplicare()
bedRepo := aplicareBed.NewRepository(aplicareClient)
bedService := aplicareBed.NewService(bedRepo)
// 4. Modul Apotek (Referensi DPHO dan Poli)
apotekClient := bpjsServices.Apotek()
dphoRepo := apotekDpho.NewRepository(apotekClient)
dphoService := apotekDpho.NewService(dphoRepo)
poliApotekRepo := apotekPoli.NewRepository(apotekClient)
poliApotekService := apotekPoli.NewService(poliApotekRepo)
// --- [NEW] Inisialisasi Modul Satu Sehat ---
satusehatFact := satuSehatFactory.NewSatuSehatFactory(cfg.SatuSehat)
satusehatClient := satusehatFact.Client()
satuSehatAuthSvc := satuSehatAuth.NewService(satusehatClient, cacheManager)
satuSehatPatientRepo := satuSehatPatient.NewRepository(satusehatClient)
satuSehatPatientSvc := satuSehatPatient.NewService(satuSehatPatientRepo)
satuSehatPractitionerRepo := satuSehatPractitioner.NewRepository(satusehatClient)
satuSehatPractitionerSvc := satuSehatPractitioner.NewService(satuSehatPractitionerRepo)
satuSehatOrganizationRepo := satuSehatOrganization.NewRepository(satusehatClient)
satuSehatOrganizationSvc := satuSehatOrganization.NewService(satuSehatOrganizationRepo)
satuSehatLocationRepo := satuSehatLocation.NewRepository(satusehatClient)
satuSehatLocationSvc := satuSehatLocation.NewService(satuSehatLocationRepo)
satuSehatKfaRepo := satuSehatKfa.NewRepository(satusehatClient)
satuSehatKfaSvc := satuSehatKfa.NewService(satuSehatKfaRepo)
satuSehatEncounterRepo := satuSehatEncounter.NewRepository(satusehatClient, dbService)
satuSehatEncounterSvc := satuSehatEncounter.NewService(satuSehatEncounterRepo)
satuSehatProcedureRepo := satuSehatProcedure.NewRepository(satusehatClient)
satuSehatProcedureSvc := satuSehatProcedure.NewService(satuSehatProcedureRepo)
satuSehatConditionRepo := satuSehatCondition.NewRepository(satusehatClient)
satuSehatConditionSvc := satuSehatCondition.NewService(satuSehatConditionRepo)
// 6. Server Orchestration (Dual Protocol & Background Workers)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Gunakan errgroup untuk mengelola lifecycle server
g, ctx := errgroup.WithContext(ctx)
// --- [NEW] Start Background Workers ---
if cfg.SatuSehat.Enabled {
imagingStudyWorkerCfg := imagingStudyWorker.Config{
DBManager: dbService,
InternalBaseURL: fmt.Sprintf("http://localhost:%d%s", cfg.Server.REST.Port, cfg.Swagger.BasePath), // otomatis: http://localhost:8080/api/v1
InternalToken: "", // Isi token di sini jika endpoint internal Usecase Anda membutuhkan Bearer Token
OrganizationID: cfg.SatuSehat.OrgID,
}
isWorker := imagingStudyWorker.NewWorker(imagingStudyWorkerCfg)
g.Go(func() error {
isWorker.Run(ctx)
return nil
})
}
serverCount := 0
// --- [A] Start REST Server ---
if cfg.Server.REST.Enabled {
serverCount++
// Menggunakan ServiceRegistry agar HTTP Server lebih rapi dan scalable
registry := &httpServer.ServiceRegistry{
Config: cfg,
DBManager: dbService,
PrimaryDB: gormDB,
CacheManager: cacheManager,
AuthService: authSvc,
Master: &httpServer.MasterServices{
RolePages: rolePagesService,
RolePermission: rolePermissionService,
RoleMaster: roleMasterService,
},
VClaim: &httpServer.VClaimServices{
Sep: sepService,
Peserta: pesertaService,
},
Antrol: &httpServer.AntrolServices{
Reference: antrolService,
},
Aplicare: &httpServer.AplicaresServices{
Bed: bedService,
},
Apotek: &httpServer.ApotekServices{
DPHO: dphoService,
Poli: poliApotekService,
},
SatuSehat: &httpServer.SatuSehatServices{
Auth: satuSehatAuthSvc,
Patient: satuSehatPatientSvc,
Practitioner: satuSehatPractitionerSvc,
Organization: satuSehatOrganizationSvc,
Location: satuSehatLocationSvc,
KFA: satuSehatKfaSvc,
Encounter: satuSehatEncounterSvc,
Procedure: satuSehatProcedureSvc,
Condition: satuSehatConditionSvc,
// TODO: Inisialisasi service sisanya di sini saat repo & service usecase lainnya sudah Anda buat, contoh:
// AllergyIntolerance: allergySvc,
// CarePlan: careplanSvc,
// ClinicalImpression: clinicalImpressionSvc,
// Composition: compositionSvc,
// DiagnosticReport: diagnosticReportSvc,
// EpisodeOfCare: episodeOfCareSvc,
// ImagingStudy: imagingStudySvc,
// Immunization: immunizationSvc,
// Medication: medicationSvc,
// MedicationDispense: medicationDispenseSvc,
// MedicationRequest: medicationRequestSvc,
// MedicationStatement: medicationStatementSvc,
// Observation: observationSvc,
// QuestionnaireResponse: questionnaireResponseSvc,
// ServiceRequest: serviceRequestSvc,
// Specimen: specimenSvc,
},
}
restSrv := httpServer.NewHTTPServer(&cfg.Server.REST, registry)
g.Go(func() error {
logger.Default().Info("REST API running", logger.Int("port", cfg.Server.REST.Port))
if err := restSrv.Start(&cfg.Server); err != nil && err != http.ErrServerClosed {
logger.Default().Error("REST Server error", logger.ErrorField(err))
return err
}
return nil
})
}
// --- [B] Start gRPC Server ---
if cfg.Server.GRPC.Enabled {
serverCount++
// 1. Buat gRPC handlers
permissionHandler := grpcHandlers.NewPermissionHandler(rolePermissionService)
// 2. Buat dan isi gRPC service registry
grpcRegistry := &grpcServers.ServiceRegistry{
PermissionHandler: permissionHandler,
}
// 3. Buat gRPC server dengan registry yang sudah diisi
grpcSrv := grpcServers.NewGRPCServer(
&cfg.Server.GRPC,
grpcRegistry,
)
g.Go(func() error {
logger.Default().Info("gRPC Server running", logger.Int("port", cfg.Server.GRPC.Port))
if err := grpcSrv.Start(); err != nil {
logger.Default().Error("gRPC Server error", logger.ErrorField(err))
return err
}
return nil
})
g.Go(func() error {
<-ctx.Done()
logger.Default().Info("Gracefully stopping gRPC Server...")
grpcSrv.Stop()
return nil
})
}
if serverCount == 0 {
logger.Default().Fatal("No server enabled (REST and gRPC are both disabled in config)")
}
// --- [C] Graceful Shutdown Listener ---
g.Go(func() error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case <-quit:
logger.Default().Warn("Signal received, shutting down servers...")
cancel()
case <-ctx.Done():
}
return nil
})
logger.Default().Info("Application started successfully")
if err := g.Wait(); err != nil {
logger.Default().Error("Server shutdown with error", logger.ErrorField(err))
} else {
logger.Default().Info("Server shutdown successful")
}
}
+83
View File
@@ -0,0 +1,83 @@
// File: /home/meninjar/goprint/service/cmd/api/main.go (Update)
package main
import (
"context"
"log"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
"service/internal/infrastructure/monitoring"
"service/pkg/logger"
// Inisialisasi master/reference Ethnic Handler
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// ... existing config loading ...
cfg := config.LoadConfig()
// 2. Init Logger dengan konfigurasi dari config
loggerConfig := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: "console",
ServiceName: "service-general",
Environment: cfg.Server.Mode,
}
logger.Init(loggerConfig)
// Initialize Prometheus Metrics
metrics := monitoring.NewMetrics("goprint-service")
// 2. Init Database
dbService := database.New(cfg)
defer dbService.Close()
// Run Database Migrations
if err := dbService.Migrate(); err != nil {
log.Printf("⚠️ Migration warning: %v", err)
}
// Get GORM DB Connection with metrics wrapper
// 3. Init Cache using factory pattern
cacheFactory := cache.NewFactory(cfg.Cache)
cacheManager, err := cacheFactory.CreateManager()
if err != nil {
log.Fatalf("❌ Failed to initialize cache manager: %v", err)
}
defer cacheManager.Close()
// Initialize Health Monitor
healthMonitor := monitoring.NewHealthMonitor(metrics, dbService, cacheManager, cfg)
// Start health monitoring in background
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go healthMonitor.Start(ctx)
// 4. Init Layers (Repo & Usecase) - Now with wrapped DB
// Update all repository initializations to use wrappedDB
// Example:
// ethnicRepo := ethnic.NewRepository(wrappedDB)
// ethnicService := ethnic.NewService(ethnicRepo)
// ethnicHandler := handlers.NewEthnicHandler(ethnicService)
// 5. Init HTTP Server with metrics
engine := gin.New()
// Add metrics middleware
engine.Use(monitoring.MetricsMiddleware(metrics))
// Register Prometheus metrics endpoint
engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
// Register health endpoint with metrics
healthMonitor.RegisterHealthEndpoint(engine)
// ... rest of the setup ...
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"service/internal/infrastructure/cache"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
httpServer "service/internal/infrastructure/transport/http/servers"
satuSehatFactory "service/internal/interfaces/satusehat"
"service/pkg/logger"
satuSehatAuth "service/internal/satusehat/reference/auth"
satuSehatKfa "service/internal/satusehat/reference/kfa"
satuSehatLocation "service/internal/satusehat/reference/location"
satuSehatOrganization "service/internal/satusehat/reference/organization"
satuSehatPatient "service/internal/satusehat/reference/patient"
satuSehatPractitioner "service/internal/satusehat/reference/practitioner"
satuSehatCondition "service/internal/satusehat/usecase/condition"
satuSehatEncounter "service/internal/satusehat/usecase/encounter"
satuSehatProcedure "service/internal/satusehat/usecase/procedure"
_ "service/docs/swagger" // Import swagger docs
"golang.org/x/sync/errgroup"
)
func main() {
// 1. Load Config
cfg := config.LoadConfig()
// 2. Init Logger khusus untuk service Satu Sehat
loggerConfig := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: "both",
ServiceName: "service-satusehat", // Penanda log
EnableCaller: cfg.Server.Mode != "production",
Environment: cfg.Server.Mode,
}
logger.Init(loggerConfig)
logger.Default().Info("SatuSehat Microservice starting...",
logger.String("environment", cfg.Server.Mode),
logger.String("log_level", cfg.Logger.Level),
)
if err := cfg.Validate(); err != nil {
logger.Default().Fatal("Config validation failed", logger.ErrorField(err))
}
// 3. Init Database Manager
dbService := database.New(cfg)
defer dbService.Close()
gormDB, err := dbService.GetGormDB("default")
if err != nil {
logger.Default().Fatal("Failed to get primary database", logger.ErrorField(err))
}
// 4. Init Cache
cacheFactory := cache.NewFactory(cfg.Cache)
cacheManager, err := cacheFactory.CreateManager()
if err != nil {
logger.Default().Fatal("Failed to initialize cache manager", logger.ErrorField(err))
}
defer cacheManager.Close()
// 5. Init Layers (Hanya Modul Satu Sehat)
satusehatFact := satuSehatFactory.NewSatuSehatFactory(cfg.SatuSehat)
satusehatClient := satusehatFact.Client()
// - Reference Services
satuSehatAuthSvc := satuSehatAuth.NewService(satusehatClient, cacheManager)
satuSehatPatientSvc := satuSehatPatient.NewService(satuSehatPatient.NewRepository(satusehatClient))
satuSehatPractitionerSvc := satuSehatPractitioner.NewService(satuSehatPractitioner.NewRepository(satusehatClient))
satuSehatOrganizationSvc := satuSehatOrganization.NewService(satuSehatOrganization.NewRepository(satusehatClient))
satuSehatLocationSvc := satuSehatLocation.NewService(satuSehatLocation.NewRepository(satusehatClient))
satuSehatKfaSvc := satuSehatKfa.NewService(satuSehatKfa.NewRepository(satusehatClient))
// - Usecase Services
satuSehatEncounterSvc := satuSehatEncounter.NewService(satuSehatEncounter.NewRepository(satusehatClient, dbService))
satuSehatProcedureSvc := satuSehatProcedure.NewService(satuSehatProcedure.NewRepository(satusehatClient))
satuSehatConditionSvc := satuSehatCondition.NewService(satuSehatCondition.NewRepository(satusehatClient))
// 6. Server Orchestration
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
g, ctx := errgroup.WithContext(ctx)
// --- Start REST Server (Hanya me-load endpoint Satu Sehat & Health) ---
if cfg.Server.REST.Enabled {
registry := &httpServer.ServiceRegistry{
Config: cfg,
DBManager: dbService,
PrimaryDB: gormDB,
CacheManager: cacheManager,
SatuSehat: &httpServer.SatuSehatServices{
Auth: satuSehatAuthSvc,
Patient: satuSehatPatientSvc,
Practitioner: satuSehatPractitionerSvc,
Organization: satuSehatOrganizationSvc,
Location: satuSehatLocationSvc,
KFA: satuSehatKfaSvc,
Encounter: satuSehatEncounterSvc,
Procedure: satuSehatProcedureSvc,
Condition: satuSehatConditionSvc,
},
}
restSrv := httpServer.NewHTTPServer(&cfg.Server.REST, registry)
g.Go(func() error {
logger.Default().Info("SatuSehat REST API running", logger.Int("port", cfg.Server.REST.Port))
if err := restSrv.Start(&cfg.Server); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})
}
// --- Graceful Shutdown Listener ---
g.Go(func() error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Default().Warn("Signal received, shutting down SatuSehat service...")
cancel()
return nil
})
if err := g.Wait(); err != nil {
logger.Default().Error("SatuSehat service shutdown with error", logger.ErrorField(err))
} else {
logger.Default().Info("SatuSehat service shutdown successful")
}
}
+9
View File
@@ -0,0 +1,9 @@
package main
import (
"service/internal/infrastructure/database/seeders"
)
func main() {
seeders.MainCLI()
}
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"service/internal/infrastructure/config"
"service/internal/infrastructure/database"
satuSehatFactory "service/internal/interfaces/satusehat"
"service/internal/worker"
"service/pkg/logger"
)
func main() {
// 1. Load Configuration
cfg := config.LoadConfig()
// 2. Initialize Logger khusus untuk Worker
loggerConfig := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: "both",
ServiceName: "service-worker", // Penanda log khusus worker
EnableCaller: cfg.Server.Mode != "production",
Environment: cfg.Server.Mode,
}
logger.Init(loggerConfig)
logger.Default().Info("Background Worker Microservice starting...",
logger.String("environment", cfg.Server.Mode),
logger.String("log_level", cfg.Logger.Level),
)
if err := cfg.Validate(); err != nil {
logger.Default().Fatal("Config validation failed", logger.ErrorField(err))
}
// 3. Initialize Database Manager
dbService := database.New(cfg)
defer dbService.Close()
// 4. Initialize SatuSehat Client (Dibutuhkan oleh worker manager)
satusehatFact := satuSehatFactory.NewSatuSehatFactory(cfg.SatuSehat)
satusehatClient := satusehatFact.Client()
// 5. Setup Context untuk Graceful Shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 6. Initialize dan Jalankan Worker Manager
workerManager := worker.NewManager(cfg, dbService, satusehatClient)
workerManager.Start(ctx)
// 7. Listener untuk OS Signals (Ctrl+C / SIGTERM)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // Block aplikasi sampai signal diterima
logger.Default().Warn("Signal received, shutting down worker service...")
cancel() // Batalkan context, trigger penghentian semua worker
}
+157
View File
@@ -0,0 +1,157 @@
# This file contains default configuration.
# Environment-specific values and secrets should be provided
# via environment variables (e.g., through a .env file).
# The application will map ENV_VARS to config keys.
# Example: 'databases.postgres.host' is overridden by 'DATABASES_POSTGRES_HOST'.
server:
# Overridden by SERVER_PORT, defaults to 8080 if not set
port: 8080
# Overridden by GIN_MODE, can be 'debug' or 'release'
mode: debug
read_timeout: 10
write_timeout: 10
# Configuration for database connections
databases:
postgres:
type: postgres
# Overridden by DATABASES_POSTGRES_HOST
host: 10.10.123.206
# Overridden by DATABASES_POSTGRES_PORT
port: 5432
# Overridden by DATABASES_POSTGRES_USERNAME
username: postgres
# Overridden by DATABASES_POSTGRES_PASSWORD
password: rss@j@y@2025 # MUST be loaded from ENV
# Overridden by DATABASES_POSTGRES_DATABASE
database: health
sslmode: disable
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
satudata:
type: postgres
host: 10.10.123.206
port: 5432
username: postgres
password: rss@j@y@2025 # MUST be loaded from ENV
database: satudata_db # Sesuaikan dengan nama database satudata Anda
sslmode: disable
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
simrs:
type: postgres
host: 10.10.123.206
port: 5432
username: postgres
password: rss@j@y@2025 # MUST be loaded from ENV
database: simrs_db # Sesuaikan dengan nama database simrs Anda
sslmode: disable
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
# Authentication configuration
auth:
# Overridden by AUTH_TYPE: 'jwt', 'keycloak', 'static', 'hybrid'
type: jwt
# Overridden by AUTH_STATIC_TOKENS: comma-separated list of tokens
static_tokens: []
# Overridden by AUTH_FALLBACK_TO: 'keycloak' or 'static'
fallback_to: ""
# Keycloak specific configuration
keycloak:
# Overridden by KEYCLOAK_ISSUER
issuer: ""
# Overridden by KEYCLOAK_AUDIENCE
audience: ""
# Overridden by KEYCLOAK_JWKS_URL
jwks_url: ""
# Overridden by KEYCLOAK_ENABLED
enabled: false
# BPJS specific configuration
bpjs:
enabled: false
# Overridden by BPJS_KDPPK
kd_ppk: ""
# Overridden by BPJS_BASEURL
base_url: https://apijkn.bpjs-kesehatan.go.id
# Overridden by BPJS_SERVICE_NAME
service_name: ""
# Overridden by BPJS_CONSID
cons_id: ""
# Overridden by BPJS_SECRETKEY
secret_key: 1bV363512D # MUST be loaded from ENV
# Overridden by BPJS_APOTEK_CONSID
apotek_cons_id: ""
# Overridden by BPJS_APOTEK_SECRETKEY
apotek_secret_key: ""
# Overridden by BPJS_VCLAIM_USERKEY
vclaim_user_key: ""
# Overridden by BPJS_ANTROL_USERKEY
antrol_user_key: 04a742fc834e0d3dc5b18913c8d46b90
# Overridden by BPJS_APOTEK_USERKEY
apotek_user_key: ""
# Overridden by BPJS_IHS_USERKEY
ihs_user_key: ""
timeout: 30s
# SatuSehat specific configuration
satu_sehat:
# Overridden by SATU_SEHAT_ORG_ID
org_id: 100026555
# Overridden by SATU_SEHAT_FASYAKES_ID
fasyakes_id: 3573011
# Overridden by SATU_SEHAT_CLIENT_ID
client_id: l1ZgJGW6K5pnrqGUikWM7fgIoquA2AQ5UUG0U8WqHaq2VEyZ
# Overridden by SATU_SEHAT_CLIENT_SECRET
client_secret: Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lGTEjjcR6zxa06ThPDy # MUST be loaded from ENV
# Overridden by SATU_SEHAT_AUTH_URL
auth_url: https://api-satusehat.kemkes.go.id/oauth2/v1
# Overridden by SATU_SEHAT_BASE_URL
base_url: https://api-satusehat.kemkes.go.id/fhir-r4/v1
# Overridden by SATU_SEHAT_CONSENT_URL
consent_url: https://api-satusehat.dto.kemkes.go.id/consent/v1
# Overridden by SATU_SEHAT_KFA_URL
kfa_url: https://api-satusehat.kemkes.go.id/kfa-v2
timeout: 30s
# Swagger documentation configuration
swagger:
title: SERVICE API
description: CUSTUM SERVICE API
version: 1.0.0
terms_of_service: http://swagger.io/terms/
contact_name: API Support
contact_url: http://rssa.example.com/support
contact_email: [email protected]
license_name: Apache 2.0
license_url: http://www.apache.org/licenses/LICENSE-2.0.html
# Overridden by SWAGGER_HOST
host: localhost:8080
# Overridden by SWAGGER_BASE_PATH
base_path: /api/v1
schemes: http,https
# Security related configuration
security:
# Overridden by SECURITY_TRUSTED_ORIGINS: comma-separated list of URLs
trusted_origins: http://localhost:3000,http://localhost:8080
max_input_length: 500
rate_limit:
# Overridden by SECURITY_RATE_LIMIT_REQUESTS_PER_MINUTE
requests_per_minute: 60
redis:
# Overridden by SECURITY_RATE_LIMIT_REDIS_HOST
host: redis # Use service name for Docker
# Overridden by SECURITY_RATE_LIMIT_REDIS_PORT
port: 6379
# Overridden by REDIS_PASSWORD
password: "" # MUST be loaded from ENV
db: 0
+1147
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
version: "3.8"
# Network akan dibuat otomatis dengan nama 'service-general_default'
# atau gunakan network yang sudah ada
networks:
# simrs-network:
# driver: bridge
# Connect to the same network as service-general Redis
general-app-network:
external: true
name: service-general_default
# Shared network for cross-container communication
shared-simrs-network:
external: true
name: shared-simrs-network
services:
app-dev:
container_name: service-satusehat
build:
context: .
dockerfile: Dockerfile.dev
env_file:
- ./.env
environment:
- CONFIG_PATH=/app/config.yaml
- REDIS_HOST=shared-redis # Redis hostname di dalam network
- REDIS_PORT=6379
- REDIS_PASSWORD=
ports:
- "8096:8096"
- "8097:8097"
volumes:
- .:/app
- /app/tmp
# depends_on:
# - redis # Redis is managed separately
networks:
- general-app-network
- shared-simrs-network
restart: unless-stopped
+83
View File
@@ -0,0 +1,83 @@
# File: /home/meninjar/goprint/service/docker-compose.monitoring.yml
version: "3.8"
services:
prometheus:
image: prom/prometheus:latest
container_name: service-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/etc/prometheus/console_libraries"
- "--web.console.templates=/etc/prometheus/consoles"
- "--storage.tsdb.retention.time=200h"
- "--web.enable-lifecycle"
networks:
- general-app-network
- shared-simrs-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: service-grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_USERS_ALLOW_SIGN_UP=false
- GF_INSTALL_PLUGINS=grafana-piechart-panel
networks:
- general-app-network
- shared-simrs-network
restart: unless-stopped
depends_on:
- prometheus
redis-exporter:
image: oliver006/redis_exporter:latest
container_name: goprint-redis-exporter
ports:
- "9121:9121"
environment:
- REDIS_ADDR=redis:6379
networks:
- goprint-network
restart: unless-stopped
depends_on:
- redis
postgres-exporter:
image: prometheuscommunity/postgres-exporter:latest
container_name: goprint-postgres-exporter
ports:
- "9187:9187"
environment:
- DATA_SOURCE_NAME=postgresql://postgres:password@postgres:5432/goprint?sslmode=disable
networks:
- goprint-network
restart: unless-stopped
depends_on:
- postgres
volumes:
prometheus_data:
grafana_data:
networks:
general-app-network:
external: true
name: service-general_default
# Shared network for cross-container communication
shared-simrs-network:
external: true
name: shared-simrs-network
+10
View File
@@ -0,0 +1,10 @@
version: "3.8"
networks:
app-network:
driver: bridge
name: general-app-network
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
+27
View File
@@ -0,0 +1,27 @@
version: "3.8"
services:
app:
container_name: service-general
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "8094:8080"
env_file:
- ./.env
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: general-redis
ports:
- "6379:6379"
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
volumes:
redis_data:
+24
View File
@@ -0,0 +1,24 @@
version: "3.8"
# Use default bridge network (no custom network needed)
services:
redis:
image: redis:7-alpine
container_name: shared-redis
ports:
- "6379:6379" # Fixed port mapping
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
# networks:
# - default # Use default network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
volumes:
redis_data:
+17
View File
@@ -0,0 +1,17 @@
# Person Service API Documentation
## Overview
This document provides comprehensive documentation for the Person Service API, including REST and gRPC endpoints.
## Table of Contents
1. [REST API Endpoints](#rest-api-endpoints)
2. [gRPC API Endpoints](#grpc-api-endpoints)
3. [Request/Response Examples](#requestresponse-examples)
4. [Error Handling](#error-handling)
5. [Authentication](#authentication)
6. [Rate Limiting](#rate-limiting)
## REST API Endpoints
### Base URL
+3
View File
@@ -0,0 +1,3 @@
# gRPC Documentation
TODO: Add documentation for gRPC services.
+36
View File
@@ -0,0 +1,36 @@
# gRPC API Endpoints - Person Service
# Generated on: $(date)
# Handler: internal/infrastructure/transport/grpc/handlers/person_handler.go
## Service: PersonService
### 1. GetPerson
- **Method**: GetPerson
- **Request**: GetPersonRequest { id: int64 }
- **Response**: GetPersonResponse { person: Person }
- **Handler**: PersonHandler.GetPerson()
### 2. ListPersons
- **Method**: ListPersons
- **Request**: ListPersonsRequest { page: int32, page_size: int32 }
- **Response**: ListPersonsResponse { persons: []Person, total: int64 }
- **Handler**: PersonHandler.ListPersons()
### 3. CreatePerson
- **Method**: CreatePerson
- **Request**: CreatePersonRequest { data: CreatePersonRequest }
- **Response**: GetPersonResponse { person: Person }
- **Handler**: PersonHandler.CreatePerson()
### 4. UpdatePerson
- **Method**: UpdatePerson
- **Request**: UpdatePersonRequest { id: int64, data: CreatePersonRequest }
- **Response**: GetPersonResponse { person: Person }
- **Handler**: PersonHandler.UpdatePerson()
### 5. DeletePerson
- **Method**: DeletePerson
- **Request**: DeletePersonRequest { id: int64 }
- **Response**: Empty {}
- **Handler**: PersonHandler.DeletePerson()
+149
View File
@@ -0,0 +1,149 @@
openapi: 3.0.0
info:
title: Person Service API
description: REST API for Person Management Service
version: 1.0.0
contact:
name: API Support
email: [email protected]
servers:
- url: http://localhost:8080/api/v1
description: Development server
paths:
/persons:
get:
summary: Get list of persons
description: Retrieve paginated list of persons
parameters:
- name: page
in: query
description: Page number
required: false
schema:
type: integer
default: 1
- name: limit
in: query
description: Items per page
required: false
schema:
type: integer
default: 10
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedPersonResponse'
post:
summary: Create new person
description: Create a new person record
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePersonRequest'
responses:
'201':
description: Person created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponse'
/persons/{id}:
get:
summary: Get person detail
description: Retrieve detailed information about a specific person
parameters:
- name: id
in: path
required: true
description: Person ID
schema:
type: integer
format: int64
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponse'
'404':
description: Person not found
put:
summary: Update person
description: Update an existing person record
parameters:
- name: id
in: path
required: true
description: Person ID
schema:
type: integer
format: int64
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePersonRequest'
responses:
'200':
description: Person updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponse'
'404':
description: Person not found
components:
schemas:
CreatePersonRequest:
type: object
properties:
name:
type: string
email:
type: string
format: email
phone:
type: string
required:
- name
- email
PersonResponse:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
email:
type: string
phone:
type: string
created_at:
type: string
format: date-time
PaginatedPersonResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/PersonResponse'
meta:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
total_pages:
type: integer
+39
View File
@@ -0,0 +1,39 @@
# REST API Endpoints - Person Service
# Generated on: $(date)
# Handler: internal/infrastructure/transport/http/handlers/person_handler.go
## Base Path: /api/v1/persons
### 1. Get List of Persons (Pagination)
- **Method**: GET
- **Path**: /persons
- **Query Parameters**:
- page (int, optional): Page number (default: 1)
- limit (int, optional): Items per page (default: 10)
- **Response**: Paginated list of persons
- **Handler**: PersonHandler.GetList()
### 2. Get Person Detail
- **Method**: GET
- **Path**: /persons/{id}
- **Path Parameters**:
- id (int64, required): Person ID
- **Response**: Single person details
- **Handler**: PersonHandler.GetDetail()
### 3. Create New Person
- **Method**: POST
- **Path**: /persons
- **Request Body**: CreatePersonRequest JSON
- **Response**: Created person details
- **Handler**: PersonHandler.Create()
### 4. Update Person
- **Method**: PUT
- **Path**: /persons/{id}
- **Path Parameters**:
- id (int64, required): Person ID
- **Request Body**: CreatePersonRequest JSON
- **Response**: Updated person details
- **Handler**: PersonHandler.Update()
+3
View File
@@ -0,0 +1,3 @@
# Architecture
TODO: Describe the project architecture.
+3
View File
@@ -0,0 +1,3 @@
# Deployment
TODO: Add deployment instructions.
+1891
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Service-General Local",
"values": [
{
"key": "base_url",
"value": "http://localhost:8080",
"enabled": true
},
{
"key": "token",
"value": "YOUR_JWT_TOKEN_HERE",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2024-01-15T00:00:00.000Z",
"_postman_exported_using": "Postman/10.0.0"
}
+22
View File
@@ -0,0 +1,22 @@
package report
// Employee merepresentasikan data dari PostgreSQL
type Employee struct {
ID int64 `gorm:"column:id"`
Name string `gorm:"column:name"`
Department string `gorm:"column:department"`
}
// Attendance merepresentasikan data dari MySQL
type Attendance struct {
EmployeeID int64 `gorm:"column:employee_id"`
Status string `gorm:"column:status"`
Date string `gorm:"column:date"`
}
// CombinedReport adalah hasil olahan (gabungan) yang akan dikembalikan ke Client
type CombinedReport struct {
EmployeeName string `json:"employee_name"`
Department string `json:"department"`
Attendances []Attendance `json:"attendances"`
}
+50
View File
@@ -0,0 +1,50 @@
package report
import (
"context"
"service/internal/infrastructure/database"
)
type Repository interface {
GetEmployeesFromPostgres(ctx context.Context) ([]Employee, error)
GetAttendancesFromMySQL(ctx context.Context, employeeIDs []int64) ([]Attendance, error)
}
type repository struct {
dbManager database.Service
pgDBName string // Nama koneksi PostgreSQL di config.yaml
mySQLName string // Nama koneksi MySQL di config.yaml
}
func NewRepository(dbManager database.Service, pgDBName, mySQLName string) Repository {
return &repository{
dbManager: dbManager,
pgDBName: pgDBName,
mySQLName: mySQLName,
}
}
// Mengakses PostgreSQL
func (r *repository) GetEmployeesFromPostgres(ctx context.Context) ([]Employee, error) {
db, err := r.dbManager.GetGormDB(r.pgDBName)
if err != nil {
return nil, err
}
var employees []Employee
err = db.WithContext(ctx).Table("employees").Find(&employees).Error
return employees, err
}
// Mengakses MySQL
func (r *repository) GetAttendancesFromMySQL(ctx context.Context, employeeIDs []int64) ([]Attendance, error) {
db, err := r.dbManager.GetGormDB(r.mySQLName)
if err != nil {
return nil, err
}
var attendances []Attendance
// Hanya ambil attendance untuk employee yang ditemukan di Postgres
err = db.WithContext(ctx).Table("attendances").Where("employee_id IN ?", employeeIDs).Find(&attendances).Error
return attendances, err
}
+61
View File
@@ -0,0 +1,61 @@
package report
import (
"context"
"service/pkg/errors"
)
type Service interface {
GenerateCrossDBReport(ctx context.Context) ([]CombinedReport, error)
}
type service struct {
repo Repository
}
func NewService(repo Repository) Service {
return &service{repo: repo}
}
func (s *service) GenerateCrossDBReport(ctx context.Context) ([]CombinedReport, error) {
// 1. Ambil data master karyawan dari PostgreSQL
employees, err := s.repo.GetEmployeesFromPostgres(ctx)
if err != nil {
return nil, errors.InternalError().Message("Failed to fetch employees from Postgres").Cause(err).Build()
}
if len(employees) == 0 {
return []CombinedReport{}, nil
}
// 2. Ekstrak ID karyawan untuk klausa IN (?) di MySQL
var empIDs []int64
for _, emp := range employees {
empIDs = append(empIDs, emp.ID)
}
// 3. Ambil data transaksi kehadiran dari MySQL menggunakan list ID di atas
attendances, err := s.repo.GetAttendancesFromMySQL(ctx, empIDs)
if err != nil {
return nil, errors.InternalError().Message("Failed to fetch attendances from MySQL").Cause(err).Build()
}
// 4. Proses In-Memory Join (Mapping)
// Kelompokkan attendance berdasarkan EmployeeID agar pencarian cepat O(1)
attendanceMap := make(map[int64][]Attendance)
for _, att := range attendances {
attendanceMap[att.EmployeeID] = append(attendanceMap[att.EmployeeID], att)
}
// Gabungkan data
var reports []CombinedReport
for _, emp := range employees {
reports = append(reports, CombinedReport{
EmployeeName: emp.Name,
Department: emp.Department,
Attendances: attendanceMap[emp.ID], // Ambil relasinya dari memory map
})
}
return reports, nil
}
+120
View File
@@ -0,0 +1,120 @@
module service
go 1.25.0
require (
github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2 // Ensure pgx is a direct dependency
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sync v0.20.0
gorm.io/driver/postgres v1.5.11 // Added GORM PostgreSQL driver
)
require (
github.com/Masterminds/squirrel v1.5.4
github.com/daku10/go-lz-string v0.0.6
github.com/gin-contrib/cors v1.7.6
github.com/go-playground/validator/v10 v10.27.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang/protobuf v1.5.4
github.com/jmoiron/sqlx v1.4.0
github.com/karincake/apem v0.0.18
github.com/lib/pq v1.10.9
github.com/minio/minio-go/v7 v7.0.98
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.17.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
go.mongodb.org/mongo-driver v1.17.6
golang.org/x/time v0.15.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/driver/sqlserver v1.6.3
gorm.io/gorm v1.30.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/microsoft/go-mssqldb v1.8.2 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+429
View File
@@ -0,0 +1,429 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daku10/go-lz-string v0.0.6 h1:aO8FFp4QPuNp7+WNyh1DyNjGF3UbZu95tUv9xOZNsYQ=
github.com/daku10/go-lz-string v0.0.6/go.mod h1:Vk++rSG3db8HXJaHEAbxiy/ukjTmPBw/iI+SrVZDzfs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/karincake/apem v0.0.18 h1:EyArN0BIqTC7P45NH0uZG5R7BGW2Wz6f4O+I766Yg+A=
github.com/karincake/apem v0.0.18/go.mod h1:cQP2sJfDrLRIiwWoaLWw/z8uAya+DWu/FpmYeinMQXM=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ=
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+31
View File
@@ -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"`
}
+13
View File
@@ -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"`
}
+242
View File
@@ -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
}
+278
View File
@@ -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
}
+9
View File
@@ -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
}
+26
View File
@@ -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
}
+24
View File
@@ -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"`
}
+60
View File
@@ -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
}
+57
View File
@@ -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
}
+53
View File
@@ -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
}
+43
View File
@@ -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
}
+136
View File
@@ -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"`
}
+67
View File
@@ -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
}
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
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
1 Code Name Status
2 001 JAWA true
3 002 SUNDA true
4 003 BATAK true
5 004 MADURA true
6 005 BUGIS true
7 006 MELAYU true
8 007 MINANG true
9 008 BETAWI true
10 009 BANJAR true
11 010 BALINESE true
12 011 ACEH true
13 012 DAYAK true
14 013 SASAK true
15 014 CHINESE true
16 015 ARAB true
17 016 INDIA true
18 017 AMBON true
19 018 PAPUA true
20 019 MINAHASA true
21 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
1 Code Name Status
2 IGD INSTALASI GAWAT DARURAT true
3 RI RAWAT INAP true
4 RJ RAWAT JALAN true
5 OK INSTALASI KAMAR OPERASI true
6 VK INSTALASI KEBIDANAN true
7 LAB INSTALASI LABORATORIUM true
8 RAD INSTALASI RADIOLOGI true
9 FAR INSTALASI FARMASI true
10 GIZI INSTALASI GIZI true
11 REHAB INSTALASI REHABILITASI true
12 INTENSIVE INSTALASI INTENSIVE true
13 NICU INSTALASI NICU true
14 PICU INSTALASI PICU true
15 ICU INSTALASI ICU true
16 OKULARIS INSTALASI OKULARIS true
17 KARDIO INSTALASI KARDIOVASKULER true
18 NEURO INSTALASI NEUROLOGI true
19 ONKO INSTALASI ONKOLOGI true
20 GERIATRI INSTALASI GERIATRI true
21 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
1 Code Name Status
2 ID INDONESIA true
3 EN ENGLISH true
4 JV JAWA true
5 SU SUNDA true
6 MA MADURA true
7 MI MINANG true
8 BT BATAK true
9 BG BUGIS true
10 ML MELAYU true
11 BN BANJAR true
12 BL BALINESE true
13 AC ACEH true
14 DY DAYAK true
15 SS SASAK true
16 ZH CHINESE true
17 AR ARABIC true
18 HI HINDI true
19 AM AMBONESE true
20 PP PAPUAN true
21 MH MINAHASA true
+35
View File
@@ -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
1 Code Name
2 11 ACEH
3 12 SUMATERA UTARA
4 13 SUMATERA BARAT
5 14 RIAU
6 15 JAMBI
7 16 SUMATERA SELATAN
8 17 BENGKULU
9 18 LAMPUNG
10 19 KEPULAUAN BANGKA BELITUNG
11 21 KEPULAUAN RIAU
12 31 DKI JAKARTA
13 32 JAWA BARAT
14 33 JAWA TENGAH
15 34 DI YOGYAKARTA
16 35 JAWA TIMUR
17 36 BANTEN
18 51 BALI
19 52 NUSA TENGGARA BARAT
20 53 NUSA TENGGARA TIMUR
21 61 KALIMANTAN BARAT
22 62 KALIMANTAN TENGAH
23 63 KALIMANTAN SELATAN
24 64 KALIMANTAN TIMUR
25 65 KALIMANTAN UTARA
26 71 SULAWESI UTARA
27 72 SULAWESI TENGAH
28 73 SULAWESI SELATAN
29 74 SULAWESI TENGGARA
30 75 GORONTALO
31 76 SULAWESI BARAT
32 81 MALUKU
33 82 MALUKU UTARA
34 91 PAPUA BARAT
35 94 PAPUA
+515
View File
@@ -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
1 Code Province_Code Name
2 1101 11 KABUPATEN SIMEULUE
3 1102 11 KABUPATEN ACEH SINGKIL
4 1103 11 KABUPATEN ACEH SELATAN
5 1104 11 KABUPATEN ACEH TENGGARA
6 1105 11 KABUPATEN ACEH TIMUR
7 1106 11 KABUPATEN ACEH TENGAH
8 1107 11 KABUPATEN ACEH BARAT
9 1108 11 KABUPATEN ACEH BESAR
10 1109 11 KABUPATEN PIDIE
11 1110 11 KABUPATEN BIREUEN
12 1111 11 KABUPATEN ACEH UTARA
13 1112 11 KABUPATEN ACEH BARAT DAYA
14 1113 11 KABUPATEN GAYO LUES
15 1114 11 KABUPATEN ACEH TAMIANG
16 1115 11 KABUPATEN NAGAN RAYA
17 1116 11 KABUPATEN ACEH JAYA
18 1117 11 KABUPATEN BENER MERIAH
19 1118 11 KABUPATEN PIDIE JAYA
20 1171 11 KOTA BANDA ACEH
21 1172 11 KOTA SABANG
22 1173 11 KOTA LANGSA
23 1174 11 KOTA LHOKSEUMAWE
24 1175 11 KOTA SUBULUSSALAM
25 1201 12 KABUPATEN NIAS
26 1202 12 KABUPATEN MANDAILING NATAL
27 1203 12 KABUPATEN TAPANULI SELATAN
28 1204 12 KABUPATEN TAPANULI TENGAH
29 1205 12 KABUPATEN TAPANULI UTARA
30 1206 12 KABUPATEN TOBA SAMOSIR
31 1207 12 KABUPATEN LABUHAN BATU
32 1208 12 KABUPATEN ASAHAN
33 1209 12 KABUPATEN SIMALUNGUN
34 1210 12 KABUPATEN DAIRI
35 1211 12 KABUPATEN KARO
36 1212 12 KABUPATEN DELI SERDANG
37 1213 12 KABUPATEN LANGKAT
38 1214 12 KABUPATEN NIAS SELATAN
39 1215 12 KABUPATEN HUMBANG HASUNDUTAN
40 1216 12 KABUPATEN PAKPAK BHARAT
41 1217 12 KABUPATEN SAMOSIR
42 1218 12 KABUPATEN SERDANG BEDAGAI
43 1219 12 KABUPATEN BATU BARA
44 1220 12 KABUPATEN PADANG LAWAS UTARA
45 1221 12 KABUPATEN PADANG LAWAS
46 1222 12 KABUPATEN LABUHAN BATU SELATAN
47 1223 12 KABUPATEN LABUHAN BATU UTARA
48 1224 12 KABUPATEN NIAS UTARA
49 1225 12 KABUPATEN NIAS BARAT
50 1271 12 KOTA SIBOLGA
51 1272 12 KOTA TANJUNG BALAI
52 1273 12 KOTA PEMATANG SIANTAR
53 1274 12 KOTA TEBING TINGGI
54 1275 12 KOTA MEDAN
55 1276 12 KOTA BINJAI
56 1277 12 KOTA PADANGSIDIMPUAN
57 1278 12 KOTA GUNUNGSITOLI
58 1301 13 KABUPATEN KEPULAUAN MENTAWAI
59 1302 13 KABUPATEN PESISIR SELATAN
60 1303 13 KABUPATEN SOLOK
61 1304 13 KABUPATEN SIJUNJUNG
62 1305 13 KABUPATEN TANAH DATAR
63 1306 13 KABUPATEN PADANG PARIAMAN
64 1307 13 KABUPATEN AGAM
65 1308 13 KABUPATEN LIMA PULUH KOTA
66 1309 13 KABUPATEN PASAMAN
67 1310 13 KABUPATEN SOLOK SELATAN
68 1311 13 KABUPATEN DHARMASRAYA
69 1312 13 KABUPATEN PASAMAN BARAT
70 1371 13 KOTA PADANG
71 1372 13 KOTA SOLOK
72 1373 13 KOTA SAWAH LUNTO
73 1374 13 KOTA PADANG PANJANG
74 1375 13 KOTA BUKITTINGGI
75 1376 13 KOTA PAYAKUMBUH
76 1377 13 KOTA PARIAMAN
77 1401 14 KABUPATEN KUANTAN SINGINGI
78 1402 14 KABUPATEN INDRAGIRI HULU
79 1403 14 KABUPATEN INDRAGIRI HILIR
80 1404 14 KABUPATEN PELALAWAN
81 1405 14 KABUPATEN S I A K
82 1406 14 KABUPATEN KAMPAR
83 1407 14 KABUPATEN ROKAN HULU
84 1408 14 KABUPATEN BENGKALIS
85 1409 14 KABUPATEN ROKAN HILIR
86 1410 14 KABUPATEN KEPULAUAN MERANTI
87 1471 14 KOTA PEKANBARU
88 1473 14 KOTA D U M A I
89 1501 15 KABUPATEN KERINCI
90 1502 15 KABUPATEN MERANGIN
91 1503 15 KABUPATEN SAROLANGUN
92 1504 15 KABUPATEN BATANG HARI
93 1505 15 KABUPATEN MUARO JAMBI
94 1506 15 KABUPATEN TANJUNG JABUNG TIMUR
95 1507 15 KABUPATEN TANJUNG JABUNG BARAT
96 1508 15 KABUPATEN TEBO
97 1509 15 KABUPATEN BUNGO
98 1571 15 KOTA JAMBI
99 1572 15 KOTA SUNGAI PENUH
100 1601 16 KABUPATEN OGAN KOMERING ULU
101 1602 16 KABUPATEN OGAN KOMERING ILIR
102 1603 16 KABUPATEN MUARA ENIM
103 1604 16 KABUPATEN LAHAT
104 1605 16 KABUPATEN MUSI RAWAS
105 1606 16 KABUPATEN MUSI BANYUASIN
106 1607 16 KABUPATEN BANYU ASIN
107 1608 16 KABUPATEN OGAN KOMERING ULU SELATAN
108 1609 16 KABUPATEN OGAN KOMERING ULU TIMUR
109 1610 16 KABUPATEN OGAN ILIR
110 1611 16 KABUPATEN EMPAT LAWANG
111 1612 16 KABUPATEN PENUKAL ABAB LEMATANG ILIR
112 1613 16 KABUPATEN MUSI RAWAS UTARA
113 1671 16 KOTA PALEMBANG
114 1672 16 KOTA PRABUMULIH
115 1673 16 KOTA PAGAR ALAM
116 1674 16 KOTA LUBUKLINGGAU
117 1701 17 KABUPATEN BENGKULU SELATAN
118 1702 17 KABUPATEN REJANG LEBONG
119 1703 17 KABUPATEN BENGKULU UTARA
120 1704 17 KABUPATEN KAUR
121 1705 17 KABUPATEN SELUMA
122 1706 17 KABUPATEN MUKOMUKO
123 1707 17 KABUPATEN LEBONG
124 1708 17 KABUPATEN KEPAHIANG
125 1709 17 KABUPATEN BENGKULU TENGAH
126 1771 17 KOTA BENGKULU
127 1801 18 KABUPATEN LAMPUNG BARAT
128 1802 18 KABUPATEN TANGGAMUS
129 1803 18 KABUPATEN LAMPUNG SELATAN
130 1804 18 KABUPATEN LAMPUNG TIMUR
131 1805 18 KABUPATEN LAMPUNG TENGAH
132 1806 18 KABUPATEN LAMPUNG UTARA
133 1807 18 KABUPATEN WAY KANAN
134 1808 18 KABUPATEN TULANGBAWANG
135 1809 18 KABUPATEN PESAWARAN
136 1810 18 KABUPATEN PRINGSEWU
137 1811 18 KABUPATEN MESUJI
138 1812 18 KABUPATEN TULANG BAWANG BARAT
139 1813 18 KABUPATEN PESISIR BARAT
140 1871 18 KOTA BANDAR LAMPUNG
141 1872 18 KOTA METRO
142 1901 19 KABUPATEN BANGKA
143 1902 19 KABUPATEN BELITUNG
144 1903 19 KABUPATEN BANGKA BARAT
145 1904 19 KABUPATEN BANGKA TENGAH
146 1905 19 KABUPATEN BANGKA SELATAN
147 1906 19 KABUPATEN BELITUNG TIMUR
148 1971 19 KOTA PANGKAL PINANG
149 2101 21 KABUPATEN KARIMUN
150 2102 21 KABUPATEN BINTAN
151 2103 21 KABUPATEN NATUNA
152 2104 21 KABUPATEN LINGGA
153 2105 21 KABUPATEN KEPULAUAN ANAMBAS
154 2171 21 KOTA BATAM
155 2172 21 KOTA TANJUNG PINANG
156 3101 31 KABUPATEN KEPULAUAN SERIBU
157 3171 31 KOTA JAKARTA SELATAN
158 3172 31 KOTA JAKARTA TIMUR
159 3173 31 KOTA JAKARTA PUSAT
160 3174 31 KOTA JAKARTA BARAT
161 3175 31 KOTA JAKARTA UTARA
162 3201 32 KABUPATEN BOGOR
163 3202 32 KABUPATEN SUKABUMI
164 3203 32 KABUPATEN CIANJUR
165 3204 32 KABUPATEN BANDUNG
166 3205 32 KABUPATEN GARUT
167 3206 32 KABUPATEN TASIKMALAYA
168 3207 32 KABUPATEN CIAMIS
169 3208 32 KABUPATEN KUNINGAN
170 3209 32 KABUPATEN CIREBON
171 3210 32 KABUPATEN MAJALENGKA
172 3211 32 KABUPATEN SUMEDANG
173 3212 32 KABUPATEN INDRAMAYU
174 3213 32 KABUPATEN SUBANG
175 3214 32 KABUPATEN PURWAKARTA
176 3215 32 KABUPATEN KARAWANG
177 3216 32 KABUPATEN BEKASI
178 3217 32 KABUPATEN BANDUNG BARAT
179 3218 32 KABUPATEN PANGANDARAN
180 3271 32 KOTA BOGOR
181 3272 32 KOTA SUKABUMI
182 3273 32 KOTA BANDUNG
183 3274 32 KOTA CIREBON
184 3275 32 KOTA BEKASI
185 3276 32 KOTA DEPOK
186 3277 32 KOTA CIMAHI
187 3278 32 KOTA TASIKMALAYA
188 3279 32 KOTA BANJAR
189 3301 33 KABUPATEN CILACAP
190 3302 33 KABUPATEN BANYUMAS
191 3303 33 KABUPATEN PURBALINGGA
192 3304 33 KABUPATEN BANJARNEGARA
193 3305 33 KABUPATEN KEBUMEN
194 3306 33 KABUPATEN PURWOREJO
195 3307 33 KABUPATEN WONOSOBO
196 3308 33 KABUPATEN MAGELANG
197 3309 33 KABUPATEN BOYOLALI
198 3310 33 KABUPATEN KLATEN
199 3311 33 KABUPATEN SUKOHARJO
200 3312 33 KABUPATEN WONOGIRI
201 3313 33 KABUPATEN KARANGANYAR
202 3314 33 KABUPATEN SRAGEN
203 3315 33 KABUPATEN GROBOGAN
204 3316 33 KABUPATEN BLORA
205 3317 33 KABUPATEN REMBANG
206 3318 33 KABUPATEN PATI
207 3319 33 KABUPATEN KUDUS
208 3320 33 KABUPATEN JEPARA
209 3321 33 KABUPATEN DEMAK
210 3322 33 KABUPATEN SEMARANG
211 3323 33 KABUPATEN TEMANGGUNG
212 3324 33 KABUPATEN KENDAL
213 3325 33 KABUPATEN BATANG
214 3326 33 KABUPATEN PEKALONGAN
215 3327 33 KABUPATEN PEMALANG
216 3328 33 KABUPATEN TEGAL
217 3329 33 KABUPATEN BREBES
218 3371 33 KOTA MAGELANG
219 3372 33 KOTA SURAKARTA
220 3373 33 KOTA SALATIGA
221 3374 33 KOTA SEMARANG
222 3375 33 KOTA PEKALONGAN
223 3376 33 KOTA TEGAL
224 3401 34 KABUPATEN KULON PROGO
225 3402 34 KABUPATEN BANTUL
226 3403 34 KABUPATEN GUNUNG KIDUL
227 3404 34 KABUPATEN SLEMAN
228 3471 34 KOTA YOGYAKARTA
229 3501 35 KABUPATEN PACITAN
230 3502 35 KABUPATEN PONOROGO
231 3503 35 KABUPATEN TRENGGALEK
232 3504 35 KABUPATEN TULUNGAGUNG
233 3505 35 KABUPATEN BLITAR
234 3506 35 KABUPATEN KEDIRI
235 3507 35 KABUPATEN MALANG
236 3508 35 KABUPATEN LUMAJANG
237 3509 35 KABUPATEN JEMBER
238 3510 35 KABUPATEN BANYUWANGI
239 3511 35 KABUPATEN BONDOWOSO
240 3512 35 KABUPATEN SITUBONDO
241 3513 35 KABUPATEN PROBOLINGGO
242 3514 35 KABUPATEN PASURUAN
243 3515 35 KABUPATEN SIDOARJO
244 3516 35 KABUPATEN MOJOKERTO
245 3517 35 KABUPATEN JOMBANG
246 3518 35 KABUPATEN NGANJUK
247 3519 35 KABUPATEN MADIUN
248 3520 35 KABUPATEN MAGETAN
249 3521 35 KABUPATEN NGAWI
250 3522 35 KABUPATEN BOJONEGORO
251 3523 35 KABUPATEN TUBAN
252 3524 35 KABUPATEN LAMONGAN
253 3525 35 KABUPATEN GRESIK
254 3526 35 KABUPATEN BANGKALAN
255 3527 35 KABUPATEN SAMPANG
256 3528 35 KABUPATEN PAMEKASAN
257 3529 35 KABUPATEN SUMENEP
258 3571 35 KOTA KEDIRI
259 3572 35 KOTA BLITAR
260 3573 35 KOTA MALANG
261 3574 35 KOTA PROBOLINGGO
262 3575 35 KOTA PASURUAN
263 3576 35 KOTA MOJOKERTO
264 3577 35 KOTA MADIUN
265 3578 35 KOTA SURABAYA
266 3579 35 KOTA BATU
267 3601 36 KABUPATEN PANDEGLANG
268 3602 36 KABUPATEN LEBAK
269 3603 36 KABUPATEN TANGERANG
270 3604 36 KABUPATEN SERANG
271 3671 36 KOTA TANGERANG
272 3672 36 KOTA CILEGON
273 3673 36 KOTA SERANG
274 3674 36 KOTA TANGERANG SELATAN
275 5101 51 KABUPATEN JEMBRANA
276 5102 51 KABUPATEN TABANAN
277 5103 51 KABUPATEN BADUNG
278 5104 51 KABUPATEN GIANYAR
279 5105 51 KABUPATEN KLUNGKUNG
280 5106 51 KABUPATEN BANGLI
281 5107 51 KABUPATEN KARANG ASEM
282 5108 51 KABUPATEN BULELENG
283 5171 51 KOTA DENPASAR
284 5201 52 KABUPATEN LOMBOK BARAT
285 5202 52 KABUPATEN LOMBOK TENGAH
286 5203 52 KABUPATEN LOMBOK TIMUR
287 5204 52 KABUPATEN SUMBAWA
288 5205 52 KABUPATEN DOMPU
289 5206 52 KABUPATEN BIMA
290 5207 52 KABUPATEN SUMBAWA BARAT
291 5208 52 KABUPATEN LOMBOK UTARA
292 5271 52 KOTA MATARAM
293 5272 52 KOTA BIMA
294 5301 53 KABUPATEN SUMBA BARAT
295 5302 53 KABUPATEN SUMBA TIMUR
296 5303 53 KABUPATEN KUPANG
297 5304 53 KABUPATEN TIMOR TENGAH SELATAN
298 5305 53 KABUPATEN TIMOR TENGAH UTARA
299 5306 53 KABUPATEN BELU
300 5307 53 KABUPATEN ALOR
301 5308 53 KABUPATEN LEMBATA
302 5309 53 KABUPATEN FLORES TIMUR
303 5310 53 KABUPATEN SIKKA
304 5311 53 KABUPATEN ENDE
305 5312 53 KABUPATEN NGADA
306 5313 53 KABUPATEN MANGGARAI
307 5314 53 KABUPATEN ROTE NDAO
308 5315 53 KABUPATEN MANGGARAI BARAT
309 5316 53 KABUPATEN SUMBA TENGAH
310 5317 53 KABUPATEN SUMBA BARAT DAYA
311 5318 53 KABUPATEN NAGEKEO
312 5319 53 KABUPATEN MANGGARAI TIMUR
313 5320 53 KABUPATEN SABU RAIJUA
314 5321 53 KABUPATEN MALAKA
315 5371 53 KOTA KUPANG
316 6101 61 KABUPATEN SAMBAS
317 6102 61 KABUPATEN BENGKAYANG
318 6103 61 KABUPATEN LANDAK
319 6104 61 KABUPATEN MEMPAWAH
320 6105 61 KABUPATEN SANGGAU
321 6106 61 KABUPATEN KETAPANG
322 6107 61 KABUPATEN SINTANG
323 6108 61 KABUPATEN KAPUAS HULU
324 6109 61 KABUPATEN SEKADAU
325 6110 61 KABUPATEN MELAWI
326 6111 61 KABUPATEN KAYONG UTARA
327 6112 61 KABUPATEN KUBU RAYA
328 6171 61 KOTA PONTIANAK
329 6172 61 KOTA SINGKAWANG
330 6201 62 KABUPATEN KOTAWARINGIN BARAT
331 6202 62 KABUPATEN KOTAWARINGIN TIMUR
332 6203 62 KABUPATEN KAPUAS
333 6204 62 KABUPATEN BARITO SELATAN
334 6205 62 KABUPATEN BARITO UTARA
335 6206 62 KABUPATEN SUKAMARA
336 6207 62 KABUPATEN LAMANDAU
337 6208 62 KABUPATEN SERUYAN
338 6209 62 KABUPATEN KATINGAN
339 6210 62 KABUPATEN PULANG PISAU
340 6211 62 KABUPATEN GUNUNG MAS
341 6212 62 KABUPATEN BARITO TIMUR
342 6213 62 KABUPATEN MURUNG RAYA
343 6271 62 KOTA PALANGKA RAYA
344 6301 63 KABUPATEN TANAH LAUT
345 6302 63 KABUPATEN KOTA BARU
346 6303 63 KABUPATEN BANJAR
347 6304 63 KABUPATEN BARITO KUALA
348 6305 63 KABUPATEN TAPIN
349 6306 63 KABUPATEN HULU SUNGAI SELATAN
350 6307 63 KABUPATEN HULU SUNGAI TENGAH
351 6308 63 KABUPATEN HULU SUNGAI UTARA
352 6309 63 KABUPATEN TABALONG
353 6310 63 KABUPATEN TANAH BUMBU
354 6311 63 KABUPATEN BALANGAN
355 6371 63 KOTA BANJARMASIN
356 6372 63 KOTA BANJAR BARU
357 6401 64 KABUPATEN PASER
358 6402 64 KABUPATEN KUTAI BARAT
359 6403 64 KABUPATEN KUTAI KARTANEGARA
360 6404 64 KABUPATEN KUTAI TIMUR
361 6405 64 KABUPATEN BERAU
362 6409 64 KABUPATEN PENAJAM PASER UTARA
363 6411 64 KABUPATEN MAHAKAM HULU
364 6471 64 KOTA BALIKPAPAN
365 6472 64 KOTA SAMARINDA
366 6474 64 KOTA BONTANG
367 6501 65 KABUPATEN MALINAU
368 6502 65 KABUPATEN BULUNGAN
369 6503 65 KABUPATEN TANA TIDUNG
370 6504 65 KABUPATEN NUNUKAN
371 6571 65 KOTA TARAKAN
372 7101 71 KABUPATEN BOLAANG MONGONDOW
373 7102 71 KABUPATEN MINAHASA
374 7103 71 KABUPATEN KEPULAUAN SANGIHE
375 7104 71 KABUPATEN KEPULAUAN TALAUD
376 7105 71 KABUPATEN MINAHASA SELATAN
377 7106 71 KABUPATEN MINAHASA UTARA
378 7107 71 KABUPATEN BOLAANG MONGONDOW UTARA
379 7108 71 KABUPATEN SIAU TAGULANDANG BIARO
380 7109 71 KABUPATEN MINAHASA TENGGARA
381 7110 71 KABUPATEN BOLAANG MONGONDOW SELATAN
382 7111 71 KABUPATEN BOLAANG MONGONDOW TIMUR
383 7171 71 KOTA MANADO
384 7172 71 KOTA BITUNG
385 7173 71 KOTA TOMOHON
386 7174 71 KOTA KOTAMOBAGU
387 7201 72 KABUPATEN BANGGAI KEPULAUAN
388 7202 72 KABUPATEN BANGGAI
389 7203 72 KABUPATEN MOROWALI
390 7204 72 KABUPATEN POSO
391 7205 72 KABUPATEN DONGGALA
392 7206 72 KABUPATEN TOLI-TOLI
393 7207 72 KABUPATEN BUOL
394 7208 72 KABUPATEN PARIGI MOUTONG
395 7209 72 KABUPATEN TOJO UNA-UNA
396 7210 72 KABUPATEN SIGI
397 7211 72 KABUPATEN BANGGAI LAUT
398 7212 72 KABUPATEN MOROWALI UTARA
399 7271 72 KOTA PALU
400 7301 73 KABUPATEN KEPULAUAN SELAYAR
401 7302 73 KABUPATEN BULUKUMBA
402 7303 73 KABUPATEN BANTAENG
403 7304 73 KABUPATEN JENEPONTO
404 7305 73 KABUPATEN TAKALAR
405 7306 73 KABUPATEN GOWA
406 7307 73 KABUPATEN SINJAI
407 7308 73 KABUPATEN MAROS
408 7309 73 KABUPATEN PANGKAJENE DAN KEPULAUAN
409 7310 73 KABUPATEN BARRU
410 7311 73 KABUPATEN BONE
411 7312 73 KABUPATEN SOPPENG
412 7313 73 KABUPATEN WAJO
413 7314 73 KABUPATEN SIDENRENG RAPPANG
414 7315 73 KABUPATEN PINRANG
415 7316 73 KABUPATEN ENREKANG
416 7317 73 KABUPATEN LUWU
417 7318 73 KABUPATEN TANA TORAJA
418 7322 73 KABUPATEN LUWU UTARA
419 7325 73 KABUPATEN LUWU TIMUR
420 7326 73 KABUPATEN TORAJA UTARA
421 7371 73 KOTA MAKASSAR
422 7372 73 KOTA PAREPARE
423 7373 73 KOTA PALOPO
424 7401 74 KABUPATEN BUTON
425 7402 74 KABUPATEN MUNA
426 7403 74 KABUPATEN KONAWE
427 7404 74 KABUPATEN KOLAKA
428 7405 74 KABUPATEN KONAWE SELATAN
429 7406 74 KABUPATEN BOMBANA
430 7407 74 KABUPATEN WAKATOBI
431 7408 74 KABUPATEN KOLAKA UTARA
432 7409 74 KABUPATEN BUTON UTARA
433 7410 74 KABUPATEN KONAWE UTARA
434 7411 74 KABUPATEN KOLAKA TIMUR
435 7412 74 KABUPATEN KONAWE KEPULAUAN
436 7413 74 KABUPATEN MUNA BARAT
437 7414 74 KABUPATEN BUTON TENGAH
438 7415 74 KABUPATEN BUTON SELATAN
439 7471 74 KOTA KENDARI
440 7472 74 KOTA BAUBAU
441 7501 75 KABUPATEN BOALEMO
442 7502 75 KABUPATEN GORONTALO
443 7503 75 KABUPATEN POHUWATO
444 7504 75 KABUPATEN BONE BOLANGO
445 7505 75 KABUPATEN GORONTALO UTARA
446 7571 75 KOTA GORONTALO
447 7601 76 KABUPATEN MAJENE
448 7602 76 KABUPATEN POLEWALI MANDAR
449 7603 76 KABUPATEN MAMASA
450 7604 76 KABUPATEN MAMUJU
451 7605 76 KABUPATEN MAMUJU UTARA
452 7606 76 KABUPATEN MAMUJU TENGAH
453 8101 81 KABUPATEN MALUKU TENGGARA BARAT
454 8102 81 KABUPATEN MALUKU TENGGARA
455 8103 81 KABUPATEN MALUKU TENGAH
456 8104 81 KABUPATEN BURU
457 8105 81 KABUPATEN KEPULAUAN ARU
458 8106 81 KABUPATEN SERAM BAGIAN BARAT
459 8107 81 KABUPATEN SERAM BAGIAN TIMUR
460 8108 81 KABUPATEN MALUKU BARAT DAYA
461 8109 81 KABUPATEN BURU SELATAN
462 8171 81 KOTA AMBON
463 8172 81 KOTA TUAL
464 8201 82 KABUPATEN HALMAHERA BARAT
465 8202 82 KABUPATEN HALMAHERA TENGAH
466 8203 82 KABUPATEN KEPULAUAN SULA
467 8204 82 KABUPATEN HALMAHERA SELATAN
468 8205 82 KABUPATEN HALMAHERA UTARA
469 8206 82 KABUPATEN HALMAHERA TIMUR
470 8207 82 KABUPATEN PULAU MOROTAI
471 8208 82 KABUPATEN PULAU TALIABU
472 8271 82 KOTA TERNATE
473 8272 82 KOTA TIDORE KEPULAUAN
474 9101 91 KABUPATEN FAKFAK
475 9102 91 KABUPATEN KAIMANA
476 9103 91 KABUPATEN TELUK WONDAMA
477 9104 91 KABUPATEN TELUK BINTUNI
478 9105 91 KABUPATEN MANOKWARI
479 9106 91 KABUPATEN SORONG SELATAN
480 9107 91 KABUPATEN SORONG
481 9108 91 KABUPATEN RAJA AMPAT
482 9109 91 KABUPATEN TAMBRAUW
483 9110 91 KABUPATEN MAYBRAT
484 9111 91 KABUPATEN MANOKWARI SELATAN
485 9112 91 KABUPATEN PEGUNUNGAN ARFAK
486 9171 91 KOTA SORONG
487 9401 94 KABUPATEN MERAUKE
488 9402 94 KABUPATEN JAYAWIJAYA
489 9403 94 KABUPATEN JAYAPURA
490 9404 94 KABUPATEN NABIRE
491 9408 94 KABUPATEN KEPULAUAN YAPEN
492 9409 94 KABUPATEN BIAK NUMFOR
493 9410 94 KABUPATEN PANIAI
494 9411 94 KABUPATEN PUNCAK JAYA
495 9412 94 KABUPATEN MIMIKA
496 9413 94 KABUPATEN BOVEN DIGOEL
497 9414 94 KABUPATEN MAPPI
498 9415 94 KABUPATEN ASMAT
499 9416 94 KABUPATEN YAHUKIMO
500 9417 94 KABUPATEN PEGUNUNGAN BINTANG
501 9418 94 KABUPATEN TOLIKARA
502 9419 94 KABUPATEN SARMI
503 9420 94 KABUPATEN KEEROM
504 9426 94 KABUPATEN WAROPEN
505 9427 94 KABUPATEN SUPIORI
506 9428 94 KABUPATEN MAMBERAMO RAYA
507 9429 94 KABUPATEN NDUGA
508 9430 94 KABUPATEN LANNY JAYA
509 9431 94 KABUPATEN MAMBERAMO TENGAH
510 9432 94 KABUPATEN YALIMO
511 9433 94 KABUPATEN PUNCAK
512 9434 94 KABUPATEN DOGIYAI
513 9435 94 KABUPATEN INTAN JAYA
514 9436 94 KABUPATEN DEIYAI
515 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
1 Code Name Status
2 SP001 DOKTER UMUM true
3 SP002 DOKTER ANAK true
4 SP003 DOKTER KANDUNGAN true
5 SP004 DOKTER BEDAH true
6 SP005 DOKTER JANTUNG true
7 SP006 DOKTER SARAF true
8 SP007 DOKTER THT true
9 SP008 DOKTER MATA true
10 SP009 DOKTER KULIT DAN KELAMIN true
11 SP010 DOKTER GIGI true
12 SP011 DOKTER PSIKIATRI true
13 SP012 DOKTER ORTHOPEDI true
14 SP013 DOKTER UROLOGI true
15 SP014 DOKTER PARU true
16 SP015 DOKTER GIZI true
17 SP016 DOKTER REHABILITASI MEDIS true
18 SP017 DOKTER GERIATRI true
19 SP018 DOKTER ONKOLOGI true
20 SP019 DOKTER KARDIOVASKULER true
21 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
1 Code Specialist_Code Name Status
2 SS001 SP001 MEDICINE true
3 SS002 SP002 PEDIATRICS true
4 SS003 SP003 OBSTETRICS true
5 SS004 SP004 SURGERY true
6 SS005 SP005 CARDIOLOGY true
7 SS006 SP006 NEUROLOGY true
8 SS007 SP007 OTORHINOLARYNGOLOGY true
9 SS008 SP008 OPHTHALMOLOGY true
10 SS009 SP009 DERMATOLOGY true
11 SS010 SP010 DENTISTRY true
12 SS011 SP011 PSYCHIATRY true
13 SS012 SP012 ORTHOPEDICS true
14 SS013 SP013 UROLOGY true
15 SS014 SP014 PULMONOLOGY true
16 SS015 SP015 NUTRITION true
17 SS016 SP016 PHYSICAL MEDICINE true
18 SS017 SP017 GERIATRICS true
19 SS018 SP018 ONCOLOGY true
20 SS019 SP019 CARDIOVASCULAR true
21 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
1 Code Name Status
2 001 POLI UMUM true
3 002 POLI ANAK true
4 003 POLI KANDUNGAN true
5 004 POLI BEDAH true
6 005 POLI JANTUNG true
7 006 POLI SARAF true
8 007 POLI THT true
9 008 POLI MATA true
10 009 POLI KULIT true
11 010 POLI GIGI true
12 011 POLI PSIKIATRI true
13 012 POLI ORTHOPEDI true
14 013 POLI UROLOGI true
15 014 POLI PARU true
16 015 POLI GIZI true
17 016 POLI REHABILITASI true
18 017 POLI GERIATRI true
19 018 POLI ONKOLOGI true
20 019 POLI KARDIOVASKULER true
21 020 POLI NEFROLOGI true
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())
}
}

Some files were not shown because too many files have changed in this diff Show More