first commit
This commit is contained in:
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
Generated
+8
@@ -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
|
||||||
Generated
+8
@@ -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>
|
||||||
Generated
+9
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+647
@@ -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
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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"
|
||||||
@@ -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
Binary file not shown.
+391
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ...
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"service/internal/infrastructure/database/seeders"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
seeders.MainCLI()
|
||||||
|
}
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# gRPC Documentation
|
||||||
|
|
||||||
|
TODO: Add documentation for gRPC services.
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
TODO: Describe the project architecture.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
TODO: Add deployment instructions.
|
||||||
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
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
// RefreshTokenRequest mendefinisikan struktur untuk request refresh token.
|
||||||
|
type RefreshTokenRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
// Provider membantu membedakan antara "jwt" dan "keycloak".
|
||||||
|
// Ini membuat endpoint menjadi fleksibel.
|
||||||
|
Provider string `json:"provider" binding:"required,oneof=jwt keycloak"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse adalah respons standar untuk login dan refresh token.
|
||||||
|
type LoginResponse struct {
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
IdToken string `json:"id_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
|
RoleID int64 `json:"role_id" binding:"required"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id int64 `json:"id" db:"id" gorm:"primaryKey"`
|
||||||
|
Email string `json:"email" db:"email"`
|
||||||
|
Password string `json:"-" db:"password"` // Hidden dari response JSON
|
||||||
|
RoleID *int64 `json:"role_id" db:"role_id"`
|
||||||
|
Active bool `json:"active" db:"active"`
|
||||||
|
CreatedAt *time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/infrastructure/database"
|
||||||
|
"service/pkg/utils/query"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandRepository interface {
|
||||||
|
CreateUser(ctx context.Context, user *User) error
|
||||||
|
UpdateUser(ctx context.Context, user *User) error
|
||||||
|
ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) error
|
||||||
|
RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryRepository interface {
|
||||||
|
FindUserByEmail(ctx context.Context, email string) (*User, error)
|
||||||
|
FindUserByID(ctx context.Context, id int64) (*User, error)
|
||||||
|
FindRefreshToken(ctx context.Context, token string) (*StoredToken, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
dbManager database.Service
|
||||||
|
dbName string
|
||||||
|
qb query.QueryBuilder
|
||||||
|
dbType query.DBType
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRepository(dbManager database.Service, dbName string) *repository {
|
||||||
|
dbType := query.DBTypePostgreSQL
|
||||||
|
allowedColumns := []string{
|
||||||
|
"Id",
|
||||||
|
"Email",
|
||||||
|
"Password",
|
||||||
|
"RoleID",
|
||||||
|
"Active",
|
||||||
|
"CreatedAt",
|
||||||
|
"UpdatedAt",
|
||||||
|
}
|
||||||
|
qb := query.NewSQLQueryBuilder(dbType).
|
||||||
|
SetSecurityOptions(true, 1000).
|
||||||
|
SetQueryLogging(true).
|
||||||
|
SetQueryTimeout(30).
|
||||||
|
SetAllowedColumns(allowedColumns)
|
||||||
|
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandRepository(dbManager database.Service, dbName string) CommandRepository {
|
||||||
|
return newRepository(dbManager, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryRepository(dbManager database.Service, dbName string) QueryRepository {
|
||||||
|
return newRepository(dbManager, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) getWriteGormDB() (*gorm.DB, error) {
|
||||||
|
return r.dbManager.GetGormDB(r.dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) getReadSQLXDB() (*sqlx.DB, error) {
|
||||||
|
sqlDB, err := r.dbManager.GetReadDB(r.dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get read db: %w", err)
|
||||||
|
}
|
||||||
|
return sqlx.NewDb(sqlDB, "pgx"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) getWriteSQLXDB() (*sqlx.DB, error) {
|
||||||
|
// Mengambil koneksi database utama (tulis)
|
||||||
|
sqlDB, err := r.dbManager.GetDB(r.dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get write db: %w", err)
|
||||||
|
}
|
||||||
|
return sqlx.NewDb(sqlDB, "pgx"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) CreateUser(ctx context.Context, user *User) error {
|
||||||
|
db, err := r.getWriteGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) UpdateUser(ctx context.Context, user *User) error {
|
||||||
|
db, err := r.getWriteGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Save(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||||
|
sqlxDB, err := r.getReadSQLXDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
q := query.DynamicQuery{
|
||||||
|
From: "users", // GORM default table name for User struct
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
query.CreateEqualFilter("Email", email),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
Limit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil // Not found is not an error, service layer will handle it
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) (err error) {
|
||||||
|
// 1. Dapatkan koneksi database tulis
|
||||||
|
writeDB, err := r.getWriteSQLXDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Mulai transaksi
|
||||||
|
tx, err := writeDB.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Defer Rollback. Ini akan dieksekusi jika fungsi return error atau panic.
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
panic(p) // Lanjutkan panic setelah rollback
|
||||||
|
} else if err != nil {
|
||||||
|
if rbErr := tx.Rollback(); rbErr != nil {
|
||||||
|
err = fmt.Errorf("transaction error: %v, rollback error: %v", err, rbErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 4. Operasi 1: Update password pengguna
|
||||||
|
updateData := query.UpdateData{
|
||||||
|
Columns: []string{"Password", "UpdatedAt"},
|
||||||
|
Values: []interface{}{newPassword, time.Now()},
|
||||||
|
}
|
||||||
|
filters := []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{query.CreateEqualFilter("Id", userID)},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Berikan objek transaksi 'tx' ke Query Builder
|
||||||
|
if _, err = r.qb.ExecuteUpdate(ctx, tx, "users", updateData, filters); err != nil {
|
||||||
|
return fmt.Errorf("failed to update password in tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Operasi 2: Catat aktivitas
|
||||||
|
logData := query.InsertData{
|
||||||
|
Columns: []string{"user_id", "activity", "created_at"},
|
||||||
|
Values: []interface{}{userID, "password changed via transaction", time.Now()},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gunakan objek transaksi 'tx' yang sama
|
||||||
|
if _, err = r.qb.ExecuteInsert(ctx, tx, "user_activity_logs", logData); err != nil {
|
||||||
|
// Asumsikan tabel 'user_activity_logs' ada
|
||||||
|
return fmt.Errorf("failed to log activity in tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Jika semua berhasil, commit transaksi
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) FindUserByID(ctx context.Context, id int64) (*User, error) {
|
||||||
|
sqlxDB, err := r.getReadSQLXDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
q := query.DynamicQuery{
|
||||||
|
From: "users", // GORM default table name for User struct
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
query.CreateEqualFilter("Id", id),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
Limit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil // Not found is not an error, service layer will handle it
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find user by id: %w", err)
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) FindRefreshToken(ctx context.Context, token string) (*StoredToken, error) {
|
||||||
|
sqlxDB, err := r.getReadSQLXDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stored StoredToken
|
||||||
|
q := query.DynamicQuery{
|
||||||
|
From: "refresh_tokens",
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{query.CreateEqualFilter("Token", token)},
|
||||||
|
}},
|
||||||
|
Limit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &stored); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find refresh token: %w", err)
|
||||||
|
}
|
||||||
|
return &stored, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error {
|
||||||
|
// Implementasi sederhana rotasi token
|
||||||
|
writeDB, err := r.getWriteGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Sesuaikan dengan struktur tabel Refresh Token yang Anda miliki di database
|
||||||
|
// Ini adalah stub untuk memenuhi kontrak interface
|
||||||
|
return writeDB.WithContext(ctx).Exec("UPDATE refresh_tokens SET is_revoked = ? WHERE token = ?", true, oldToken).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"service/internal/infrastructure/cache"
|
||||||
|
"service/internal/infrastructure/config"
|
||||||
|
"service/pkg/errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoredToken struct {
|
||||||
|
Token string
|
||||||
|
IsRevoked bool
|
||||||
|
UserID int64
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *StoredToken) IsExpired() bool { return false /* implementasi */ }
|
||||||
|
|
||||||
|
// Service mendefinisikan interface untuk operasi otentikasi.
|
||||||
|
type Service interface {
|
||||||
|
RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error)
|
||||||
|
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
||||||
|
Register(ctx context.Context, req RegisterRequest) (interface{}, error)
|
||||||
|
Logout(ctx context.Context, token string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
cmdRepo CommandRepository
|
||||||
|
queryRepo QueryRepository
|
||||||
|
cache *cache.Manager
|
||||||
|
config *config.Config // Ganti keycloakClient dengan config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService membuat instance service auth baru.
|
||||||
|
// Signature diperbarui untuk menerima config secara keseluruhan.
|
||||||
|
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, cache *cache.Manager, cfg *config.Config) Service {
|
||||||
|
return &service{
|
||||||
|
cmdRepo: cmdRepo,
|
||||||
|
queryRepo: queryRepo,
|
||||||
|
cache: cache,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login mengimplementasikan otentikasi menggunakan data dummy untuk keperluan testing.
|
||||||
|
func (s *service) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||||
|
// Simulasi pengecekan kredensial dengan beberapa akun dummy
|
||||||
|
type dummyUser struct {
|
||||||
|
Password string
|
||||||
|
UserID string
|
||||||
|
RoleID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
dummyUsers := map[string]dummyUser{
|
||||||
|
"[email protected]": {"password123", "1001", "admin", "Admin Dummy"},
|
||||||
|
"[email protected]": {"password123", "1002", "user", "User Dummy"},
|
||||||
|
"[email protected]": {"password123", "1003", "manager", "Manager Dummy"},
|
||||||
|
}
|
||||||
|
|
||||||
|
user, exists := dummyUsers[req.Email]
|
||||||
|
if !exists || user.Password != req.Password {
|
||||||
|
return nil, errors.UnauthorizedError().Message("Email atau password salah. Coba: [email protected], [email protected], [email protected] (password123)").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := os.Getenv("JWT_SECRET")
|
||||||
|
if secret == "" {
|
||||||
|
secret = "fallback_secret_key_change_in_production"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||||
|
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": user.UserID,
|
||||||
|
"email": req.Email,
|
||||||
|
"role_id": user.RoleID,
|
||||||
|
"name": user.Name,
|
||||||
|
"exp": time.Now().Add(60 * time.Minute).Unix(),
|
||||||
|
})
|
||||||
|
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||||
|
|
||||||
|
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||||
|
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": user.UserID,
|
||||||
|
"email": req.Email,
|
||||||
|
"role_id": user.RoleID,
|
||||||
|
"name": user.Name,
|
||||||
|
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||||
|
})
|
||||||
|
newRefreshTokenStr, _ := refreshToken.SignedString([]byte(secret))
|
||||||
|
|
||||||
|
return &LoginResponse{
|
||||||
|
Provider: "jwt",
|
||||||
|
AccessToken: newAccessTokenStr,
|
||||||
|
RefreshToken: newRefreshTokenStr,
|
||||||
|
ExpiresIn: 900, // 15 menit dalam detik
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register mengimplementasikan pendaftaran user dengan respons data json dummy.
|
||||||
|
func (s *service) Register(ctx context.Context, req RegisterRequest) (interface{}, error) {
|
||||||
|
// Mengembalikan response dummy seolah-olah user berhasil dibuat di DB
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": 1002, // Generated Dummy ID
|
||||||
|
"name": req.Name,
|
||||||
|
"email": req.Email,
|
||||||
|
"role_id": req.RoleID,
|
||||||
|
"created_at": time.Now().Format(time.RFC3339),
|
||||||
|
"status": "active",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout mengimplementasikan proses logout dengan memasukkan token ke cache blacklist.
|
||||||
|
func (s *service) Logout(ctx context.Context, token string) error {
|
||||||
|
if token == "" {
|
||||||
|
return errors.NewValidationError().Message("Token tidak valid").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate token di cache (Blacklist) jika cache manager aktif
|
||||||
|
if s.cache != nil {
|
||||||
|
// Set token ke dalam blacklist dengan durasi 15 menit (Sesuai umur access token)
|
||||||
|
_ = s.cache.Set(ctx, "blacklist_token:"+token, true, 15*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Segera hapus/tandai Refresh Token sebagai revoked di database agar tidak bisa
|
||||||
|
// digunakan lagi untuk generate access token baru setelah pengguna berhasil logout.
|
||||||
|
// _ = s.cmdRepo.RevokeAllTokensByToken(ctx, token)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken menangani refresh token untuk berbagai provider.
|
||||||
|
func (s *service) RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error) {
|
||||||
|
switch req.Provider {
|
||||||
|
case "jwt":
|
||||||
|
return s.refreshInternalJWT(ctx, req.RefreshToken)
|
||||||
|
case "keycloak":
|
||||||
|
return s.refreshKeycloakToken(ctx, req.RefreshToken)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.NewValidationError().Message("Provider tidak didukung").Metadata("provider", req.Provider).Build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshInternalJWT menangani validasi dan pembuatan JWT internal baru.
|
||||||
|
func (s *service) refreshInternalJWT(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||||
|
secret := os.Getenv("JWT_SECRET")
|
||||||
|
if secret == "" {
|
||||||
|
secret = "fallback_secret_key_change_in_production"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse dan validasi refresh token
|
||||||
|
parsedToken, err := jwt.Parse(refreshToken, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method")
|
||||||
|
}
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil || !parsedToken.Valid {
|
||||||
|
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau telah kedaluwarsa").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.UnauthorizedError().Message("Klaim token tidak valid").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah ini dummy user dari fungsi Login. Jika ya, lewati pengecekan DB.
|
||||||
|
isDummyUser := false
|
||||||
|
if userID, ok := claims["user_id"].(string); ok && (userID == "1001" || userID == "1002" || userID == "1003") {
|
||||||
|
isDummyUser = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hanya lakukan pengecekan ke database jika bukan dummy user
|
||||||
|
if !isDummyUser {
|
||||||
|
existingToken, err := s.queryRepo.FindRefreshToken(ctx, refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
// Kesalahan saat query ke database
|
||||||
|
return nil, errors.InternalError().Message("Gagal memvalidasi refresh token").Cause(err).Build()
|
||||||
|
}
|
||||||
|
if existingToken == nil {
|
||||||
|
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau tidak ditemukan").Build()
|
||||||
|
}
|
||||||
|
if existingToken.IsRevoked {
|
||||||
|
return nil, errors.UnauthorizedError().Message("Refresh token telah dicabut (dibatalkan)").Metadata("reason", "revoked").Build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||||
|
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": claims["user_id"],
|
||||||
|
"email": claims["email"],
|
||||||
|
"role_id": claims["role_id"],
|
||||||
|
"name": claims["name"],
|
||||||
|
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||||
|
})
|
||||||
|
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||||
|
|
||||||
|
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||||
|
newRefreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": claims["user_id"],
|
||||||
|
"email": claims["email"],
|
||||||
|
"role_id": claims["role_id"],
|
||||||
|
"name": claims["name"],
|
||||||
|
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||||
|
})
|
||||||
|
newRefreshTokenStr, _ := newRefreshToken.SignedString([]byte(secret))
|
||||||
|
|
||||||
|
// Revoke token lama dan simpan yang baru (hanya untuk user non-dummy)
|
||||||
|
if !isDummyUser {
|
||||||
|
_ = s.cmdRepo.RevokeAndSaveTokens(ctx, refreshToken, newRefreshTokenStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LoginResponse{
|
||||||
|
Provider: "jwt",
|
||||||
|
AccessToken: newAccessTokenStr,
|
||||||
|
RefreshToken: newRefreshTokenStr,
|
||||||
|
ExpiresIn: 900, // 15 menit dalam detik
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshKeycloakToken menangani refresh token melalui Keycloak.
|
||||||
|
func (s *service) refreshKeycloakToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||||
|
if !s.config.Keycloak.Enabled || s.config.Keycloak.URL == "" {
|
||||||
|
return nil, errors.InternalError().Message("Provider Keycloak tidak dikonfigurasi dengan benar").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", s.config.Keycloak.URL, s.config.Keycloak.Realm)
|
||||||
|
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("grant_type", "refresh_token")
|
||||||
|
data.Set("client_id", s.config.Keycloak.ClientID)
|
||||||
|
data.Set("client_secret", s.config.Keycloak.ClientSecret)
|
||||||
|
data.Set("refresh_token", refreshToken)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Cause(err).Message("Gagal membuat request ke Keycloak").Build()
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalError().Cause(err).Message("Gagal berkomunikasi dengan Keycloak").Build()
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var keycloakErr map[string]interface{}
|
||||||
|
_ = json.NewDecoder(resp.Body).Decode(&keycloakErr)
|
||||||
|
return nil, errors.UnauthorizedError().
|
||||||
|
Message("Gagal me-refresh token dengan Keycloak").
|
||||||
|
Metadata("upstream_status", resp.StatusCode).
|
||||||
|
Metadata("upstream_error", keycloakErr).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResponse LoginResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
|
||||||
|
return nil, errors.InternalError().Cause(err).Message("Gagal mem-parsing respons Keycloak").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan provider ke response
|
||||||
|
tokenResponse.Provider = "keycloak"
|
||||||
|
|
||||||
|
return &tokenResponse, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package antrol
|
||||||
|
|
||||||
|
// Poli merepresentasikan data referensi poli dari API Antrean RS BPJS
|
||||||
|
type Poli struct {
|
||||||
|
KdPoli string `json:"kdpoli"`
|
||||||
|
NmPoli string `json:"nmpoli"`
|
||||||
|
KdSubSpesialis string `json:"kdsubspesialis"`
|
||||||
|
NmSubSpesialis string `json:"nmsubspesialis"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package antrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
GetRefPoli(ctx context.Context) ([]Poli, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefPoli fetches referensi poli from BPJS API
|
||||||
|
//
|
||||||
|
// # This function will return an array of Poli and error if any
|
||||||
|
//
|
||||||
|
// Context is used to pass the request context to the underlying
|
||||||
|
// client
|
||||||
|
//
|
||||||
|
// The function will return an error if the request to BPJS API
|
||||||
|
// fails or if the response cannot be unmarshalled into an array
|
||||||
|
// of Poli
|
||||||
|
//
|
||||||
|
// The function will return an empty array and nil error if the request
|
||||||
|
// to BPJS API succeeds but the response is an empty array
|
||||||
|
func (r *repository) GetRefPoli(ctx context.Context) ([]Poli, error) {
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", "ref/poli", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []Poli
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal ref poli antrol response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package antrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetRefPoli(ctx context.Context) ([]Poli, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetRefPoli(ctx context.Context) ([]Poli, error) {
|
||||||
|
res, err := s.repo.GetRefPoli(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil referensi poli Antrean RS BPJS").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package bed
|
||||||
|
|
||||||
|
// BedData merepresentasikan payload untuk membuat, mengubah, atau mengambil data ketersediaan Bed
|
||||||
|
type BedData struct {
|
||||||
|
KodeKelas string `json:"kodekelas"`
|
||||||
|
KodeRuang string `json:"koderuang"`
|
||||||
|
NamaRuang string `json:"namaruang"`
|
||||||
|
Kapasitas int `json:"kapasitas"`
|
||||||
|
Tersedia int `json:"tersedia"`
|
||||||
|
TersediaPria int `json:"tersediapria"`
|
||||||
|
TersediaWanita int `json:"tersediawanita"`
|
||||||
|
TersediaPriaWanita int `json:"tersediapriawanita"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BedDeletePayload merepresentasikan payload untuk menghapus data Bed
|
||||||
|
type BedDeletePayload struct {
|
||||||
|
KodeKelas string `json:"kodekelas"`
|
||||||
|
KodeRuang string `json:"koderuang"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BedReadResponse merepresentasikan balikan dari endpoint GET Read Bed
|
||||||
|
type BedReadResponse struct {
|
||||||
|
List []BedData `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package bed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
Read(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error)
|
||||||
|
Create(ctx context.Context, kdPpk string, payload BedData) error
|
||||||
|
Update(ctx context.Context, kdPpk string, payload BedData) error
|
||||||
|
Delete(ctx context.Context, kdPpk string, payload BedDeletePayload) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) Read(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error) {
|
||||||
|
endpoint := fmt.Sprintf("rest/bed/read/%s/%d/%d", kdPpk, start, limit)
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result BedReadResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal applicare read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.List, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) Create(ctx context.Context, kdPpk string, payload BedData) error {
|
||||||
|
endpoint := fmt.Sprintf("rest/bed/create/%s", kdPpk)
|
||||||
|
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||||
|
// Aplicares biasanya mengembalikan sukses di metadata yang sudah dihandle oleh client.go
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) Update(ctx context.Context, kdPpk string, payload BedData) error {
|
||||||
|
// Endpoint Update Aplicares menggunakan method POST, bukan PUT
|
||||||
|
endpoint := fmt.Sprintf("rest/bed/update/%s", kdPpk)
|
||||||
|
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) Delete(ctx context.Context, kdPpk string, payload BedDeletePayload) error {
|
||||||
|
// Endpoint Delete Aplicares juga menggunakan method POST
|
||||||
|
endpoint := fmt.Sprintf("rest/bed/delete/%s", kdPpk)
|
||||||
|
_, err := r.client.DoRequest(ctx, "POST", endpoint, payload)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package bed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetBedList(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error)
|
||||||
|
CreateBed(ctx context.Context, kdPpk string, req BedData) error
|
||||||
|
UpdateBed(ctx context.Context, kdPpk string, req BedData) error
|
||||||
|
DeleteBed(ctx context.Context, kdPpk string, req BedDeletePayload) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetBedList(ctx context.Context, kdPpk string, start, limit int) ([]BedData, error) {
|
||||||
|
if kdPpk == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Kode PPK (Kode Faskes) wajib diisi").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.repo.Read(ctx, kdPpk, start, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil data ketersediaan tempat tidur (Aplicares)").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) CreateBed(ctx context.Context, kdPpk string, req BedData) error {
|
||||||
|
err := s.repo.Create(ctx, kdPpk, req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ExternalError().Message("Gagal membuat data tempat tidur baru (Aplicares)").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateBed(ctx context.Context, kdPpk string, req BedData) error {
|
||||||
|
err := s.repo.Update(ctx, kdPpk, req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ExternalError().Message("Gagal memperbarui data tempat tidur (Aplicares)").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) DeleteBed(ctx context.Context, kdPpk string, req BedDeletePayload) error {
|
||||||
|
err := s.repo.Delete(ctx, kdPpk, req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ExternalError().Message("Gagal menghapus data tempat tidur (Aplicares)").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dpho
|
||||||
|
|
||||||
|
// DPHOData merepresentasikan detail referensi DPHO dari API Apotek
|
||||||
|
type DPHOData struct {
|
||||||
|
KodeObat string `json:"kodeobat"`
|
||||||
|
NamaObat string `json:"namaobat"`
|
||||||
|
PRB string `json:"prb"`
|
||||||
|
Kronis string `json:"kronis"`
|
||||||
|
Kemo string `json:"kemo"`
|
||||||
|
Harga string `json:"harga"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package dpho
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
GetDPHO(ctx context.Context) ([]DPHOData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GetDPHO(ctx context.Context) ([]DPHOData, error) {
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", "referensi/dpho", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coba parsing ke dalam format object { "list": [...] }
|
||||||
|
var result struct {
|
||||||
|
List []DPHOData `json:"list"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
// Fallback jika API mengembalikan array langsung [...]
|
||||||
|
var directList []DPHOData
|
||||||
|
if err2 := json.Unmarshal(respBytes, &directList); err2 == nil {
|
||||||
|
return directList, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal apotek dpho response: %w", err)
|
||||||
|
}
|
||||||
|
return result.List, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package dpho
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetDPHO(ctx context.Context) ([]DPHOData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetDPHO(ctx context.Context) ([]DPHOData, error) {
|
||||||
|
res, err := s.repo.GetDPHO(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil referensi DPHO Apotek BPJS").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package poli
|
||||||
|
|
||||||
|
// PoliData merepresentasikan detail referensi Poli dari API Apotek
|
||||||
|
type PoliData struct {
|
||||||
|
KodePoli string `json:"kodepoli"`
|
||||||
|
NamaPoli string `json:"namapoli"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package poli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
GetPoli(ctx context.Context, param string) ([]PoliData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GetPoli(ctx context.Context, param string) ([]PoliData, error) {
|
||||||
|
endpoint := fmt.Sprintf("referensi/poli/%s", param)
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
List []PoliData `json:"list"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
var directList []PoliData
|
||||||
|
if err2 := json.Unmarshal(respBytes, &directList); err2 == nil {
|
||||||
|
return directList, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal apotek poli response: %w", err)
|
||||||
|
}
|
||||||
|
return result.List, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package poli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetPoli(ctx context.Context, param string) ([]PoliData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetPoli(ctx context.Context, param string) ([]PoliData, error) {
|
||||||
|
if param == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Parameter pencarian Poli wajib diisi").Build()
|
||||||
|
}
|
||||||
|
res, err := s.repo.GetPoli(ctx, param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil referensi Poli Apotek BPJS").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package peserta
|
||||||
|
|
||||||
|
// PesertaResponse merepresentasikan wrapper data dari API BPJS VClaim
|
||||||
|
type PesertaResponse struct {
|
||||||
|
Peserta PesertaData `json:"peserta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PesertaData merepresentasikan entitas detail Peserta BPJS
|
||||||
|
type PesertaData struct {
|
||||||
|
NoKartu string `json:"noKartu"`
|
||||||
|
Nik string `json:"nik"`
|
||||||
|
Nama string `json:"nama"`
|
||||||
|
Pisa string `json:"pisa"`
|
||||||
|
Sex string `json:"sex"`
|
||||||
|
Umur Umur `json:"umur"`
|
||||||
|
TglLahir string `json:"tglLahir"`
|
||||||
|
StatusPeserta StatusPeserta `json:"statusPeserta"`
|
||||||
|
ProviderUmum ProviderUmum `json:"provUmum"`
|
||||||
|
JenisPeserta JenisPeserta `json:"jenisPeserta"`
|
||||||
|
HakKelas HakKelas `json:"hakKelas"`
|
||||||
|
Informasi Informasi `json:"informasi"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Umur struct {
|
||||||
|
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
|
||||||
|
UmurSekarang string `json:"umurSekarang"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusPeserta struct {
|
||||||
|
Kode string `json:"kode"`
|
||||||
|
Keterangan string `json:"keterangan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderUmum struct {
|
||||||
|
KdProvider string `json:"kdProvider"`
|
||||||
|
NmProvider string `json:"nmProvider"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JenisPeserta struct {
|
||||||
|
Kode string `json:"kode"`
|
||||||
|
Keterangan string `json:"keterangan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HakKelas struct {
|
||||||
|
Kode string `json:"kode"`
|
||||||
|
Keterangan string `json:"keterangan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Informasi struct {
|
||||||
|
Dinsos string `json:"dinsos"`
|
||||||
|
ProlanisPRB string `json:"prolanisPRB"`
|
||||||
|
NoSKTM string `json:"noSKTM"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package peserta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
GetByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error)
|
||||||
|
GetByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GetByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error) {
|
||||||
|
endpoint := fmt.Sprintf("Peserta/nokartu/%s/tglSEP/%s", noKartu, tglSEP)
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result PesertaResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal peserta response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.Peserta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GetByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error) {
|
||||||
|
endpoint := fmt.Sprintf("Peserta/nik/%s/tglSEP/%s", nik, tglSEP)
|
||||||
|
respBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result PesertaResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal peserta response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.Peserta, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package peserta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"service/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetPesertaByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error)
|
||||||
|
GetPesertaByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetPesertaByNoKartu(ctx context.Context, noKartu, tglSEP string) (*PesertaData, error) {
|
||||||
|
if noKartu == "" || tglSEP == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("Nomor Kartu dan Tanggal SEP wajib diisi").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.repo.GetByNoKartu(ctx, noKartu, tglSEP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil data peserta BPJS berdasarkan No Kartu").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetPesertaByNIK(ctx context.Context, nik, tglSEP string) (*PesertaData, error) {
|
||||||
|
if nik == "" || tglSEP == "" {
|
||||||
|
return nil, errors.NewValidationError().Message("NIK dan Tanggal SEP wajib diisi").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.repo.GetByNIK(ctx, nik, tglSEP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ExternalError().Message("Gagal mengambil data peserta BPJS berdasarkan NIK").Cause(err).Build()
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package sep
|
||||||
|
|
||||||
|
// CreateSEPRequest adalah data payload untuk insert pembuatan SEP.
|
||||||
|
// Strukturnya mengikuti format yang dibutuhkan oleh API VClaim 2.0.
|
||||||
|
type CreateSEPRequest struct {
|
||||||
|
Request struct {
|
||||||
|
Sep `json:"t_sep"`
|
||||||
|
} `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sep adalah detail data untuk pembuatan SEP.
|
||||||
|
type Sep struct {
|
||||||
|
NoKartu string `json:"noKartu"`
|
||||||
|
TglSep string `json:"tglSep"`
|
||||||
|
PpkPelayanan string `json:"ppkPelayanan"`
|
||||||
|
JnsPelayanan string `json:"jnsPelayanan"`
|
||||||
|
KlsRawat KlsRawat `json:"klsRawat"`
|
||||||
|
NoMR string `json:"noMR"`
|
||||||
|
Rujukan Rujukan `json:"rujukan"`
|
||||||
|
Catatan string `json:"catatan"`
|
||||||
|
DiagAwal string `json:"diagAwal"`
|
||||||
|
Poli Poli `json:"poli"`
|
||||||
|
Cob string `json:"cob"`
|
||||||
|
Katarak string `json:"katarak"`
|
||||||
|
Jaminan Jaminan `json:"jaminan"`
|
||||||
|
Tujuan string `json:"tujuan"`
|
||||||
|
FlagProcedure string `json:"flagProcedure"`
|
||||||
|
KdPenunjang string `json:"kdPenunjang"`
|
||||||
|
AssesmentPel string `json:"assesmentPel"`
|
||||||
|
NoSurat string `json:"noSurat"`
|
||||||
|
KodeDPJP string `json:"kodeDPJP"`
|
||||||
|
DpjpLayan string `json:"dpjpLayan"`
|
||||||
|
NoTelp string `json:"noTelp"`
|
||||||
|
User string `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KlsRawat struct {
|
||||||
|
KlsRawatHak string `json:"klsRawatHak"`
|
||||||
|
KlsRawatNaik string `json:"klsRawatNaik,omitempty"`
|
||||||
|
Pembiayaan string `json:"pembiayaan,omitempty"`
|
||||||
|
PenanggungJawab string `json:"penanggungJawab,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rujukan struct {
|
||||||
|
AsalRujukan string `json:"asalRujukan"`
|
||||||
|
TglRujukan string `json:"tglRujukan"`
|
||||||
|
NoRujukan string `json:"noRujukan"`
|
||||||
|
PpkRujukan string `json:"ppkRujukan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Poli struct {
|
||||||
|
Tujuan string `json:"tujuan"`
|
||||||
|
Eksekutif string `json:"eksekutif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Jaminan struct {
|
||||||
|
LakaLantas string `json:"lakaLantas"`
|
||||||
|
NoLP string `json:"noLP,omitempty"`
|
||||||
|
Penjamin Penjamin `json:"penjamin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Penjamin struct {
|
||||||
|
TglKejadian string `json:"tglKejadian,omitempty"`
|
||||||
|
Keterangan string `json:"keterangan,omitempty"`
|
||||||
|
Suplesi struct {
|
||||||
|
Suplesi string `json:"suplesi"`
|
||||||
|
NoSepSuplesi string `json:"noSepSuplesi,omitempty"`
|
||||||
|
LokasiLaka struct {
|
||||||
|
KdPropinsi string `json:"kdPropinsi"`
|
||||||
|
KdKabupaten string `json:"kdKabupaten"`
|
||||||
|
KdKecamatan string `json:"kdKecamatan"`
|
||||||
|
} `json:"lokasiLaka,omitempty"`
|
||||||
|
} `json:"suplesi,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSEPResponse adalah data balikan setelah SEP berhasil dicreate.
|
||||||
|
type CreateSEPResponse struct {
|
||||||
|
Sep struct {
|
||||||
|
NoSep string `json:"noSep"`
|
||||||
|
TglSep string `json:"tglSep"`
|
||||||
|
Poli string `json:"poli"`
|
||||||
|
Diagnosa string `json:"diagnosa"`
|
||||||
|
Catatan string `json:"catatan"`
|
||||||
|
JnsRawat string `json:"jnsRawat"`
|
||||||
|
KlsRawat string `json:"klsRawat"`
|
||||||
|
Penjamin string `json:"penjamin"`
|
||||||
|
} `json:"sep"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSEPRequest adalah data payload untuk update SEP.
|
||||||
|
type UpdateSEPRequest struct {
|
||||||
|
Request struct {
|
||||||
|
Sep struct {
|
||||||
|
NoSep string `json:"noSep"`
|
||||||
|
KlsRawat KlsRawat `json:"klsRawat"`
|
||||||
|
NoMR string `json:"noMR"`
|
||||||
|
Catatan string `json:"catatan"`
|
||||||
|
DiagAwal string `json:"diagAwal"`
|
||||||
|
Poli Poli `json:"poli"`
|
||||||
|
Eksekutif string `json:"eksekutif"`
|
||||||
|
Cob string `json:"cob"`
|
||||||
|
Katarak string `json:"katarak"`
|
||||||
|
Jaminan Jaminan `json:"jaminan"`
|
||||||
|
NoTelp string `json:"noTelp"`
|
||||||
|
User string `json:"user"`
|
||||||
|
} `json:"t_sep"`
|
||||||
|
} `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSEPRequest adalah data payload untuk menghapus SEP.
|
||||||
|
type DeleteSEPRequest struct {
|
||||||
|
Request struct {
|
||||||
|
Sep struct {
|
||||||
|
NoSep string `json:"noSep"`
|
||||||
|
User string `json:"user"`
|
||||||
|
} `json:"t_sep"`
|
||||||
|
} `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEPDetailResponse adalah data balikan untuk detail SEP.
|
||||||
|
type SEPDetailResponse struct {
|
||||||
|
Catatan string `json:"catatan"`
|
||||||
|
Diagnosa string `json:"diagnosa"`
|
||||||
|
JnsPelayanan string `json:"jnsPelayanan"`
|
||||||
|
KelasRawat string `json:"kelasRawat"`
|
||||||
|
NoRujukan string `json:"noRujukan"`
|
||||||
|
NoSep string `json:"noSep"`
|
||||||
|
Penjamin string `json:"penjamin"`
|
||||||
|
Peserta struct {
|
||||||
|
Nama string `json:"nama"`
|
||||||
|
NoKartu string `json:"noKartu"`
|
||||||
|
NoMr string `json:"noMr"`
|
||||||
|
} `json:"peserta"`
|
||||||
|
Poli string `json:"poli"`
|
||||||
|
TglSep string `json:"tglSep"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package sep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"service/internal/interfaces/bpjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
CreateSEP(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error)
|
||||||
|
UpdateSEP(ctx context.Context, req UpdateSEPRequest) (string, error)
|
||||||
|
DeleteSEP(ctx context.Context, req DeleteSEPRequest) (string, error)
|
||||||
|
GetSEP(ctx context.Context, noSEP string) (*SEPDetailResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
client bpjs.BpjsClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(client bpjs.BpjsClient) Repository {
|
||||||
|
return &repository{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) CreateSEP(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error) {
|
||||||
|
endpoint := "SEP/2.0/insert"
|
||||||
|
decryptedBytes, err := r.client.DoRequest(ctx, "POST", endpoint, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result CreateSEPResponse
|
||||||
|
return &result, json.Unmarshal(decryptedBytes, &result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) UpdateSEP(ctx context.Context, req UpdateSEPRequest) (string, error) {
|
||||||
|
endpoint := "SEP/2.0/update"
|
||||||
|
decryptedBytes, err := r.client.DoRequest(ctx, "PUT", endpoint, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Response dari update SEP biasanya hanya string nomor SEP itu sendiri
|
||||||
|
return string(decryptedBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) DeleteSEP(ctx context.Context, req DeleteSEPRequest) (string, error) {
|
||||||
|
endpoint := "SEP/2.0/delete"
|
||||||
|
// BPJS API untuk delete menggunakan POST method dengan payload spesifik
|
||||||
|
decryptedBytes, err := r.client.DoRequest(ctx, "POST", endpoint, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Response dari delete SEP biasanya hanya string "OK" atau pesan konfirmasi
|
||||||
|
return string(decryptedBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) GetSEP(ctx context.Context, noSEP string) (*SEPDetailResponse, error) {
|
||||||
|
endpoint := fmt.Sprintf("SEP/%s", noSEP)
|
||||||
|
decryptedBytes, err := r.client.DoRequest(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result SEPDetailResponse
|
||||||
|
if err := json.Unmarshal(decryptedBytes, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal SEP detail response: %w", err)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package sep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Create(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error)
|
||||||
|
Update(ctx context.Context, req UpdateSEPRequest) (string, error)
|
||||||
|
Delete(ctx context.Context, req DeleteSEPRequest) (string, error)
|
||||||
|
GetDetail(ctx context.Context, noSEP string) (*SEPDetailResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo Repository) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Create(ctx context.Context, req CreateSEPRequest) (*CreateSEPResponse, error) {
|
||||||
|
// Anda bisa menambahkan validasi atau logika bisnis di sini sebelum memanggil repository.
|
||||||
|
return s.repo.CreateSEP(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Update(ctx context.Context, req UpdateSEPRequest) (string, error) {
|
||||||
|
// TODO: Tambahkan validasi untuk request update di sini.
|
||||||
|
// Contoh: if req.Request.Sep.NoSep == "" { return "", errors.New("nomor SEP wajib diisi") }
|
||||||
|
return s.repo.UpdateSEP(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(ctx context.Context, req DeleteSEPRequest) (string, error) {
|
||||||
|
// TODO: Tambahkan validasi untuk request delete di sini.
|
||||||
|
return s.repo.DeleteSEP(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetDetail(ctx context.Context, noSEP string) (*SEPDetailResponse, error) {
|
||||||
|
// TODO: Tambahkan validasi untuk noSEP di sini.
|
||||||
|
return s.repo.GetSEP(ctx, noSEP)
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/cache.go
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache interface defines the contract for cache implementations
|
||||||
|
type Cache interface {
|
||||||
|
// Basic operations
|
||||||
|
Get(ctx context.Context, key string) (string, error)
|
||||||
|
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
Exists(ctx context.Context, key string) (bool, error)
|
||||||
|
|
||||||
|
// Struct operations
|
||||||
|
GetStruct(ctx context.Context, key string, dest interface{}) error
|
||||||
|
SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error
|
||||||
|
|
||||||
|
// Numeric operations
|
||||||
|
Increment(ctx context.Context, key string, delta int64) (int64, error)
|
||||||
|
Decrement(ctx context.Context, key string, delta int64) (int64, error)
|
||||||
|
|
||||||
|
// Utility operations
|
||||||
|
TTL(ctx context.Context, key string) (time.Duration, error)
|
||||||
|
Close() error
|
||||||
|
Health(ctx context.Context) error
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
MGet(ctx context.Context, keys ...string) ([]interface{}, error)
|
||||||
|
MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheConfig holds configuration for cache
|
||||||
|
type CacheConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
DefaultTTL time.Duration
|
||||||
|
SessionTTL time.Duration
|
||||||
|
RateLimitTTL time.Duration
|
||||||
|
CleanupInterval time.Duration
|
||||||
|
MaxRetries int
|
||||||
|
RetryDelay time.Duration
|
||||||
|
Redis RedisConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Password string
|
||||||
|
DB int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common cache key prefixes
|
||||||
|
const (
|
||||||
|
KeyPrefixSession = "session:"
|
||||||
|
KeyPrefixUser = "user:"
|
||||||
|
KeyPrefixToken = "token:"
|
||||||
|
KeyPrefixRateLimit = "rate_limit:"
|
||||||
|
KeyPrefixCache = "cache:"
|
||||||
|
KeyPrefixTemp = "temp:"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common expiration times
|
||||||
|
const (
|
||||||
|
ExpirationSession = 24 * time.Hour
|
||||||
|
ExpirationShort = 5 * time.Minute
|
||||||
|
ExpirationMedium = 30 * time.Minute
|
||||||
|
ExpirationLong = 2 * time.Hour
|
||||||
|
ExpirationVeryLong = 24 * time.Hour
|
||||||
|
)
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/factory.go
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/infrastructure/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Factory creates cache instances based on configuration
|
||||||
|
type Factory struct {
|
||||||
|
config config.CacheConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFactory creates a new cache factory
|
||||||
|
func NewFactory(cfg config.CacheConfig) *Factory {
|
||||||
|
return &Factory{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a cache instance based on configuration
|
||||||
|
func (f *Factory) Create() (Cache, error) {
|
||||||
|
if !f.config.Enabled {
|
||||||
|
return NewNoOpCache(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, only Redis is supported
|
||||||
|
return NewRedisCache(CacheConfig{
|
||||||
|
Enabled: f.config.Enabled,
|
||||||
|
DefaultTTL: f.config.DefaultTTL,
|
||||||
|
SessionTTL: f.config.SessionTTL,
|
||||||
|
RateLimitTTL: f.config.RateLimitTTL,
|
||||||
|
CleanupInterval: f.config.CleanupInterval,
|
||||||
|
MaxRetries: f.config.MaxRetries,
|
||||||
|
RetryDelay: f.config.RetryDelay,
|
||||||
|
Redis: RedisConfig{
|
||||||
|
Host: f.config.Redis.Host,
|
||||||
|
Port: f.config.Redis.Port,
|
||||||
|
Password: f.config.Redis.Password,
|
||||||
|
DB: f.config.Redis.DB,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateManager creates a cache manager with the configured cache
|
||||||
|
func (f *Factory) CreateManager() (*Manager, error) {
|
||||||
|
cache, err := f.Create()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewManager(cache, CacheConfig{
|
||||||
|
Enabled: f.config.Enabled,
|
||||||
|
DefaultTTL: f.config.DefaultTTL,
|
||||||
|
SessionTTL: f.config.SessionTTL,
|
||||||
|
RateLimitTTL: f.config.RateLimitTTL,
|
||||||
|
CleanupInterval: f.config.CleanupInterval,
|
||||||
|
MaxRetries: f.config.MaxRetries,
|
||||||
|
RetryDelay: f.config.RetryDelay,
|
||||||
|
Redis: RedisConfig{
|
||||||
|
Host: f.config.Redis.Host,
|
||||||
|
Port: f.config.Redis.Port,
|
||||||
|
Password: f.config.Redis.Password,
|
||||||
|
DB: f.config.Redis.DB,
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager provides high-level cache operations
|
||||||
|
type Manager struct {
|
||||||
|
cache Cache
|
||||||
|
config CacheConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisClientProvider interface untuk cache yang menyediakan Redis client
|
||||||
|
type RedisClientProvider interface {
|
||||||
|
GetRedisClient() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new cache manager
|
||||||
|
func NewManager(cache Cache, config CacheConfig) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
cache: cache,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session operations
|
||||||
|
func (m *Manager) SetSession(ctx context.Context, sessionID string, userData interface{}) error {
|
||||||
|
key := KeyPrefixSession + sessionID
|
||||||
|
return m.cache.SetStruct(ctx, key, userData, m.config.SessionTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetSession(ctx context.Context, sessionID string, dest interface{}) error {
|
||||||
|
key := KeyPrefixSession + sessionID
|
||||||
|
return m.cache.GetStruct(ctx, key, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
|
key := KeyPrefixSession + sessionID
|
||||||
|
return m.cache.Delete(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting operations
|
||||||
|
func (m *Manager) IncrementRateLimit(ctx context.Context, identifier string) (int64, error) {
|
||||||
|
key := KeyPrefixRateLimit + identifier
|
||||||
|
return m.cache.Increment(ctx, key, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetRateLimit(ctx context.Context, identifier string) (int64, error) {
|
||||||
|
key := KeyPrefixRateLimit + identifier
|
||||||
|
// Try to get as string first, then convert
|
||||||
|
value, err := m.cache.Get(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
_, err = fmt.Sscanf(value, "%d", &count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ResetRateLimit(ctx context.Context, identifier string) error {
|
||||||
|
key := KeyPrefixRateLimit + identifier
|
||||||
|
return m.cache.Delete(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetRateLimit(ctx context.Context, identifier string, count int64) error {
|
||||||
|
key := KeyPrefixRateLimit + identifier
|
||||||
|
value := fmt.Sprintf("%d", count)
|
||||||
|
return m.cache.Set(ctx, key, value, m.config.RateLimitTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cache operations
|
||||||
|
func (m *Manager) SetUser(ctx context.Context, userID string, userData interface{}) error {
|
||||||
|
key := KeyPrefixUser + userID
|
||||||
|
return m.cache.SetStruct(ctx, key, userData, m.config.DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetUser(ctx context.Context, userID string, dest interface{}) error {
|
||||||
|
key := KeyPrefixUser + userID
|
||||||
|
return m.cache.GetStruct(ctx, key, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DeleteUser(ctx context.Context, userID string) error {
|
||||||
|
key := KeyPrefixUser + userID
|
||||||
|
return m.cache.Delete(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token operations
|
||||||
|
func (m *Manager) SetToken(ctx context.Context, token string, tokenData interface{}, expiration time.Duration) error {
|
||||||
|
key := KeyPrefixToken + token
|
||||||
|
return m.cache.SetStruct(ctx, key, tokenData, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetToken(ctx context.Context, token string, dest interface{}) error {
|
||||||
|
key := KeyPrefixToken + token
|
||||||
|
return m.cache.GetStruct(ctx, key, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DeleteToken(ctx context.Context, token string) error {
|
||||||
|
key := KeyPrefixToken + token
|
||||||
|
return m.cache.Delete(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic cache operations with TTL
|
||||||
|
func (m *Manager) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||||
|
return m.cache.SetStruct(ctx, key, value, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(ctx context.Context, key string, dest interface{}) error {
|
||||||
|
return m.cache.GetStruct(ctx, key, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Delete(ctx context.Context, key string) error {
|
||||||
|
return m.cache.Delete(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
|
return m.cache.Exists(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
func (m *Manager) Health(ctx context.Context) error {
|
||||||
|
return m.cache.Health(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedisClient mendapatkan Redis client jika tersedia
|
||||||
|
func (m *Manager) GetRedisClient() interface{} {
|
||||||
|
if redisProvider, ok := m.cache.(RedisClientProvider); ok {
|
||||||
|
return redisProvider.GetRedisClient()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the cache connection
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
return m.cache.Close()
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/noop.go
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NoOpCache is a no-operation cache implementation for development
|
||||||
|
type NoOpCache struct{}
|
||||||
|
|
||||||
|
// NewNoOpCache creates a new no-op cache instance
|
||||||
|
func NewNoOpCache() Cache {
|
||||||
|
return &NoOpCache{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get always returns empty string and error
|
||||||
|
func (n *NoOpCache) Get(ctx context.Context, key string) (string, error) {
|
||||||
|
return "", errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set always succeeds but does nothing
|
||||||
|
func (n *NoOpCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete always succeeds but does nothing
|
||||||
|
func (n *NoOpCache) Delete(ctx context.Context, key string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists always returns false
|
||||||
|
func (n *NoOpCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStruct always returns error
|
||||||
|
func (n *NoOpCache) GetStruct(ctx context.Context, key string, dest interface{}) error {
|
||||||
|
return errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStruct always succeeds but does nothing
|
||||||
|
func (n *NoOpCache) SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment always returns 0 and error
|
||||||
|
func (n *NoOpCache) Increment(ctx context.Context, key string, delta int64) (int64, error) {
|
||||||
|
return 0, errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement always returns 0 and error
|
||||||
|
func (n *NoOpCache) Decrement(ctx context.Context, key string, delta int64) (int64, error) {
|
||||||
|
return 0, errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTL always returns 0 and error
|
||||||
|
func (n *NoOpCache) TTL(ctx context.Context, key string) (time.Duration, error) {
|
||||||
|
return 0, errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close always succeeds
|
||||||
|
func (n *NoOpCache) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health always succeeds
|
||||||
|
func (n *NoOpCache) Health(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGet always returns empty slice and error
|
||||||
|
func (n *NoOpCache) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
|
||||||
|
return []interface{}{}, errors.New("cache disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSet always succeeds but does nothing
|
||||||
|
func (n *NoOpCache) MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
// File: /home/meninjar/goprint/service-general/internal/infrastructure/cache/redis.go
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisCache implements Cache interface using Redis
|
||||||
|
type RedisCache struct {
|
||||||
|
client *redis.Client
|
||||||
|
config CacheConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisCache creates a new Redis cache instance
|
||||||
|
func NewRedisCache(config CacheConfig) (Cache, error) {
|
||||||
|
if !config.Enabled {
|
||||||
|
return &NoOpCache{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port),
|
||||||
|
Password: config.Redis.Password,
|
||||||
|
DB: config.Redis.DB,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := client.Ping(ctx).Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RedisCache{
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value from cache
|
||||||
|
func (r *RedisCache) Get(ctx context.Context, key string) (string, error) {
|
||||||
|
return r.client.Get(ctx, key).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores a value in cache
|
||||||
|
func (r *RedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||||
|
return r.client.Set(ctx, key, value, expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a value from cache
|
||||||
|
func (r *RedisCache) Delete(ctx context.Context, key string) error {
|
||||||
|
return r.client.Del(ctx, key).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists checks if a key exists in cache
|
||||||
|
func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
|
result, err := r.client.Exists(ctx, key).Result()
|
||||||
|
return result > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStruct retrieves and unmarshals a struct from cache
|
||||||
|
func (r *RedisCache) GetStruct(ctx context.Context, key string, dest interface{}) error {
|
||||||
|
data, err := r.client.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal([]byte(data), dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStruct marshals and stores a struct in cache
|
||||||
|
func (r *RedisCache) SetStruct(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal value: %w", err)
|
||||||
|
}
|
||||||
|
return r.client.Set(ctx, key, data, expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment increments a numeric value in cache
|
||||||
|
func (r *RedisCache) Increment(ctx context.Context, key string, delta int64) (int64, error) {
|
||||||
|
return r.client.IncrBy(ctx, key, delta).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement decrements a numeric value in cache
|
||||||
|
func (r *RedisCache) Decrement(ctx context.Context, key string, delta int64) (int64, error) {
|
||||||
|
return r.client.DecrBy(ctx, key, delta).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTL returns the time to live for a key
|
||||||
|
func (r *RedisCache) TTL(ctx context.Context, key string) (time.Duration, error) {
|
||||||
|
return r.client.TTL(ctx, key).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedisClient mendapatkan Redis client
|
||||||
|
func (r *RedisCache) GetRedisClient() interface{} {
|
||||||
|
return r.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Redis connection
|
||||||
|
func (r *RedisCache) Close() error {
|
||||||
|
return r.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health checks the health of the cache
|
||||||
|
func (r *RedisCache) Health(ctx context.Context) error {
|
||||||
|
return r.client.Ping(ctx).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGet retrieves multiple values from cache
|
||||||
|
func (r *RedisCache) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
|
||||||
|
return r.client.MGet(ctx, keys...).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSet stores multiple values in cache
|
||||||
|
func (r *RedisCache) MSet(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||||
|
pipe := r.client.Pipeline()
|
||||||
|
|
||||||
|
for key, value := range items {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal value for key %s: %w", key, err)
|
||||||
|
}
|
||||||
|
pipe.Set(ctx, key, data, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
// internal/infrastructure/container/container.go
|
||||||
|
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"service/internal/infrastructure/cache"
|
||||||
|
"service/internal/infrastructure/config"
|
||||||
|
"service/pkg/logger" // Pastikan import ini benar
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container holds all application dependencies
|
||||||
|
type Container struct {
|
||||||
|
Config *config.Config
|
||||||
|
CacheManager *cache.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContainer creates a new container with all dependencies
|
||||||
|
func NewContainer(cfg *config.Config) (*Container, error) {
|
||||||
|
// Create cache factory
|
||||||
|
cacheFactory := cache.NewFactory(cfg.Cache)
|
||||||
|
|
||||||
|
// Create cache manager
|
||||||
|
cacheManager, err := cacheFactory.CreateManager()
|
||||||
|
if err != nil {
|
||||||
|
// PERBAIKAN: Gunakan logger baru yang terstruktur
|
||||||
|
logger.Default().Fatal("Failed to create cache manager", logger.ErrorField(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERBAIKAN: Gunakan logger baru yang terstruktur
|
||||||
|
logger.Default().Info("Cache manager initialized successfully")
|
||||||
|
|
||||||
|
return &Container{
|
||||||
|
Config: cfg,
|
||||||
|
CacheManager: cacheManager,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all container resources
|
||||||
|
func (c *Container) Close() error {
|
||||||
|
if c.CacheManager != nil {
|
||||||
|
if err := c.CacheManager.Close(); err != nil {
|
||||||
|
// PERBAIKAN: Gunakan logger baru, log error sebelum mengembalikan
|
||||||
|
logger.Default().Error("Failed to close cache manager", logger.ErrorField(err))
|
||||||
|
return fmt.Errorf("failed to close cache manager: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+7216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Name,Status
|
||||||
|
001,JAWA,true
|
||||||
|
002,SUNDA,true
|
||||||
|
003,BATAK,true
|
||||||
|
004,MADURA,true
|
||||||
|
005,BUGIS,true
|
||||||
|
006,MELAYU,true
|
||||||
|
007,MINANG,true
|
||||||
|
008,BETAWI,true
|
||||||
|
009,BANJAR,true
|
||||||
|
010,BALINESE,true
|
||||||
|
011,ACEH,true
|
||||||
|
012,DAYAK,true
|
||||||
|
013,SASAK,true
|
||||||
|
014,CHINESE,true
|
||||||
|
015,ARAB,true
|
||||||
|
016,INDIA,true
|
||||||
|
017,AMBON,true
|
||||||
|
018,PAPUA,true
|
||||||
|
019,MINAHASA,true
|
||||||
|
020,TORAJA,true
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Name,Status
|
||||||
|
IGD,INSTALASI GAWAT DARURAT,true
|
||||||
|
RI,RAWAT INAP,true
|
||||||
|
RJ,RAWAT JALAN,true
|
||||||
|
OK,INSTALASI KAMAR OPERASI,true
|
||||||
|
VK,INSTALASI KEBIDANAN,true
|
||||||
|
LAB,INSTALASI LABORATORIUM,true
|
||||||
|
RAD,INSTALASI RADIOLOGI,true
|
||||||
|
FAR,INSTALASI FARMASI,true
|
||||||
|
GIZI,INSTALASI GIZI,true
|
||||||
|
REHAB,INSTALASI REHABILITASI,true
|
||||||
|
INTENSIVE,INSTALASI INTENSIVE,true
|
||||||
|
NICU,INSTALASI NICU,true
|
||||||
|
PICU,INSTALASI PICU,true
|
||||||
|
ICU,INSTALASI ICU,true
|
||||||
|
OKULARIS,INSTALASI OKULARIS,true
|
||||||
|
KARDIO,INSTALASI KARDIOVASKULER,true
|
||||||
|
NEURO,INSTALASI NEUROLOGI,true
|
||||||
|
ONKO,INSTALASI ONKOLOGI,true
|
||||||
|
GERIATRI,INSTALASI GERIATRI,true
|
||||||
|
PSIKIATRI,INSTALASI PSIKIATRI,true
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Name,Status
|
||||||
|
ID,INDONESIA,true
|
||||||
|
EN,ENGLISH,true
|
||||||
|
JV,JAWA,true
|
||||||
|
SU,SUNDA,true
|
||||||
|
MA,MADURA,true
|
||||||
|
MI,MINANG,true
|
||||||
|
BT,BATAK,true
|
||||||
|
BG,BUGIS,true
|
||||||
|
ML,MELAYU,true
|
||||||
|
BN,BANJAR,true
|
||||||
|
BL,BALINESE,true
|
||||||
|
AC,ACEH,true
|
||||||
|
DY,DAYAK,true
|
||||||
|
SS,SASAK,true
|
||||||
|
ZH,CHINESE,true
|
||||||
|
AR,ARABIC,true
|
||||||
|
HI,HINDI,true
|
||||||
|
AM,AMBONESE,true
|
||||||
|
PP,PAPUAN,true
|
||||||
|
MH,MINAHASA,true
|
||||||
|
+35
@@ -0,0 +1,35 @@
|
|||||||
|
Code,Name
|
||||||
|
11,ACEH
|
||||||
|
12,SUMATERA UTARA
|
||||||
|
13,SUMATERA BARAT
|
||||||
|
14,RIAU
|
||||||
|
15,JAMBI
|
||||||
|
16,SUMATERA SELATAN
|
||||||
|
17,BENGKULU
|
||||||
|
18,LAMPUNG
|
||||||
|
19,KEPULAUAN BANGKA BELITUNG
|
||||||
|
21,KEPULAUAN RIAU
|
||||||
|
31,DKI JAKARTA
|
||||||
|
32,JAWA BARAT
|
||||||
|
33,JAWA TENGAH
|
||||||
|
34,DI YOGYAKARTA
|
||||||
|
35,JAWA TIMUR
|
||||||
|
36,BANTEN
|
||||||
|
51,BALI
|
||||||
|
52,NUSA TENGGARA BARAT
|
||||||
|
53,NUSA TENGGARA TIMUR
|
||||||
|
61,KALIMANTAN BARAT
|
||||||
|
62,KALIMANTAN TENGAH
|
||||||
|
63,KALIMANTAN SELATAN
|
||||||
|
64,KALIMANTAN TIMUR
|
||||||
|
65,KALIMANTAN UTARA
|
||||||
|
71,SULAWESI UTARA
|
||||||
|
72,SULAWESI TENGAH
|
||||||
|
73,SULAWESI SELATAN
|
||||||
|
74,SULAWESI TENGGARA
|
||||||
|
75,GORONTALO
|
||||||
|
76,SULAWESI BARAT
|
||||||
|
81,MALUKU
|
||||||
|
82,MALUKU UTARA
|
||||||
|
91,PAPUA BARAT
|
||||||
|
94,PAPUA
|
||||||
|
+515
@@ -0,0 +1,515 @@
|
|||||||
|
Code,Province_Code,Name
|
||||||
|
1101,11,KABUPATEN SIMEULUE
|
||||||
|
1102,11,KABUPATEN ACEH SINGKIL
|
||||||
|
1103,11,KABUPATEN ACEH SELATAN
|
||||||
|
1104,11,KABUPATEN ACEH TENGGARA
|
||||||
|
1105,11,KABUPATEN ACEH TIMUR
|
||||||
|
1106,11,KABUPATEN ACEH TENGAH
|
||||||
|
1107,11,KABUPATEN ACEH BARAT
|
||||||
|
1108,11,KABUPATEN ACEH BESAR
|
||||||
|
1109,11,KABUPATEN PIDIE
|
||||||
|
1110,11,KABUPATEN BIREUEN
|
||||||
|
1111,11,KABUPATEN ACEH UTARA
|
||||||
|
1112,11,KABUPATEN ACEH BARAT DAYA
|
||||||
|
1113,11,KABUPATEN GAYO LUES
|
||||||
|
1114,11,KABUPATEN ACEH TAMIANG
|
||||||
|
1115,11,KABUPATEN NAGAN RAYA
|
||||||
|
1116,11,KABUPATEN ACEH JAYA
|
||||||
|
1117,11,KABUPATEN BENER MERIAH
|
||||||
|
1118,11,KABUPATEN PIDIE JAYA
|
||||||
|
1171,11,KOTA BANDA ACEH
|
||||||
|
1172,11,KOTA SABANG
|
||||||
|
1173,11,KOTA LANGSA
|
||||||
|
1174,11,KOTA LHOKSEUMAWE
|
||||||
|
1175,11,KOTA SUBULUSSALAM
|
||||||
|
1201,12,KABUPATEN NIAS
|
||||||
|
1202,12,KABUPATEN MANDAILING NATAL
|
||||||
|
1203,12,KABUPATEN TAPANULI SELATAN
|
||||||
|
1204,12,KABUPATEN TAPANULI TENGAH
|
||||||
|
1205,12,KABUPATEN TAPANULI UTARA
|
||||||
|
1206,12,KABUPATEN TOBA SAMOSIR
|
||||||
|
1207,12,KABUPATEN LABUHAN BATU
|
||||||
|
1208,12,KABUPATEN ASAHAN
|
||||||
|
1209,12,KABUPATEN SIMALUNGUN
|
||||||
|
1210,12,KABUPATEN DAIRI
|
||||||
|
1211,12,KABUPATEN KARO
|
||||||
|
1212,12,KABUPATEN DELI SERDANG
|
||||||
|
1213,12,KABUPATEN LANGKAT
|
||||||
|
1214,12,KABUPATEN NIAS SELATAN
|
||||||
|
1215,12,KABUPATEN HUMBANG HASUNDUTAN
|
||||||
|
1216,12,KABUPATEN PAKPAK BHARAT
|
||||||
|
1217,12,KABUPATEN SAMOSIR
|
||||||
|
1218,12,KABUPATEN SERDANG BEDAGAI
|
||||||
|
1219,12,KABUPATEN BATU BARA
|
||||||
|
1220,12,KABUPATEN PADANG LAWAS UTARA
|
||||||
|
1221,12,KABUPATEN PADANG LAWAS
|
||||||
|
1222,12,KABUPATEN LABUHAN BATU SELATAN
|
||||||
|
1223,12,KABUPATEN LABUHAN BATU UTARA
|
||||||
|
1224,12,KABUPATEN NIAS UTARA
|
||||||
|
1225,12,KABUPATEN NIAS BARAT
|
||||||
|
1271,12,KOTA SIBOLGA
|
||||||
|
1272,12,KOTA TANJUNG BALAI
|
||||||
|
1273,12,KOTA PEMATANG SIANTAR
|
||||||
|
1274,12,KOTA TEBING TINGGI
|
||||||
|
1275,12,KOTA MEDAN
|
||||||
|
1276,12,KOTA BINJAI
|
||||||
|
1277,12,KOTA PADANGSIDIMPUAN
|
||||||
|
1278,12,KOTA GUNUNGSITOLI
|
||||||
|
1301,13,KABUPATEN KEPULAUAN MENTAWAI
|
||||||
|
1302,13,KABUPATEN PESISIR SELATAN
|
||||||
|
1303,13,KABUPATEN SOLOK
|
||||||
|
1304,13,KABUPATEN SIJUNJUNG
|
||||||
|
1305,13,KABUPATEN TANAH DATAR
|
||||||
|
1306,13,KABUPATEN PADANG PARIAMAN
|
||||||
|
1307,13,KABUPATEN AGAM
|
||||||
|
1308,13,KABUPATEN LIMA PULUH KOTA
|
||||||
|
1309,13,KABUPATEN PASAMAN
|
||||||
|
1310,13,KABUPATEN SOLOK SELATAN
|
||||||
|
1311,13,KABUPATEN DHARMASRAYA
|
||||||
|
1312,13,KABUPATEN PASAMAN BARAT
|
||||||
|
1371,13,KOTA PADANG
|
||||||
|
1372,13,KOTA SOLOK
|
||||||
|
1373,13,KOTA SAWAH LUNTO
|
||||||
|
1374,13,KOTA PADANG PANJANG
|
||||||
|
1375,13,KOTA BUKITTINGGI
|
||||||
|
1376,13,KOTA PAYAKUMBUH
|
||||||
|
1377,13,KOTA PARIAMAN
|
||||||
|
1401,14,KABUPATEN KUANTAN SINGINGI
|
||||||
|
1402,14,KABUPATEN INDRAGIRI HULU
|
||||||
|
1403,14,KABUPATEN INDRAGIRI HILIR
|
||||||
|
1404,14,KABUPATEN PELALAWAN
|
||||||
|
1405,14,KABUPATEN S I A K
|
||||||
|
1406,14,KABUPATEN KAMPAR
|
||||||
|
1407,14,KABUPATEN ROKAN HULU
|
||||||
|
1408,14,KABUPATEN BENGKALIS
|
||||||
|
1409,14,KABUPATEN ROKAN HILIR
|
||||||
|
1410,14,KABUPATEN KEPULAUAN MERANTI
|
||||||
|
1471,14,KOTA PEKANBARU
|
||||||
|
1473,14,KOTA D U M A I
|
||||||
|
1501,15,KABUPATEN KERINCI
|
||||||
|
1502,15,KABUPATEN MERANGIN
|
||||||
|
1503,15,KABUPATEN SAROLANGUN
|
||||||
|
1504,15,KABUPATEN BATANG HARI
|
||||||
|
1505,15,KABUPATEN MUARO JAMBI
|
||||||
|
1506,15,KABUPATEN TANJUNG JABUNG TIMUR
|
||||||
|
1507,15,KABUPATEN TANJUNG JABUNG BARAT
|
||||||
|
1508,15,KABUPATEN TEBO
|
||||||
|
1509,15,KABUPATEN BUNGO
|
||||||
|
1571,15,KOTA JAMBI
|
||||||
|
1572,15,KOTA SUNGAI PENUH
|
||||||
|
1601,16,KABUPATEN OGAN KOMERING ULU
|
||||||
|
1602,16,KABUPATEN OGAN KOMERING ILIR
|
||||||
|
1603,16,KABUPATEN MUARA ENIM
|
||||||
|
1604,16,KABUPATEN LAHAT
|
||||||
|
1605,16,KABUPATEN MUSI RAWAS
|
||||||
|
1606,16,KABUPATEN MUSI BANYUASIN
|
||||||
|
1607,16,KABUPATEN BANYU ASIN
|
||||||
|
1608,16,KABUPATEN OGAN KOMERING ULU SELATAN
|
||||||
|
1609,16,KABUPATEN OGAN KOMERING ULU TIMUR
|
||||||
|
1610,16,KABUPATEN OGAN ILIR
|
||||||
|
1611,16,KABUPATEN EMPAT LAWANG
|
||||||
|
1612,16,KABUPATEN PENUKAL ABAB LEMATANG ILIR
|
||||||
|
1613,16,KABUPATEN MUSI RAWAS UTARA
|
||||||
|
1671,16,KOTA PALEMBANG
|
||||||
|
1672,16,KOTA PRABUMULIH
|
||||||
|
1673,16,KOTA PAGAR ALAM
|
||||||
|
1674,16,KOTA LUBUKLINGGAU
|
||||||
|
1701,17,KABUPATEN BENGKULU SELATAN
|
||||||
|
1702,17,KABUPATEN REJANG LEBONG
|
||||||
|
1703,17,KABUPATEN BENGKULU UTARA
|
||||||
|
1704,17,KABUPATEN KAUR
|
||||||
|
1705,17,KABUPATEN SELUMA
|
||||||
|
1706,17,KABUPATEN MUKOMUKO
|
||||||
|
1707,17,KABUPATEN LEBONG
|
||||||
|
1708,17,KABUPATEN KEPAHIANG
|
||||||
|
1709,17,KABUPATEN BENGKULU TENGAH
|
||||||
|
1771,17,KOTA BENGKULU
|
||||||
|
1801,18,KABUPATEN LAMPUNG BARAT
|
||||||
|
1802,18,KABUPATEN TANGGAMUS
|
||||||
|
1803,18,KABUPATEN LAMPUNG SELATAN
|
||||||
|
1804,18,KABUPATEN LAMPUNG TIMUR
|
||||||
|
1805,18,KABUPATEN LAMPUNG TENGAH
|
||||||
|
1806,18,KABUPATEN LAMPUNG UTARA
|
||||||
|
1807,18,KABUPATEN WAY KANAN
|
||||||
|
1808,18,KABUPATEN TULANGBAWANG
|
||||||
|
1809,18,KABUPATEN PESAWARAN
|
||||||
|
1810,18,KABUPATEN PRINGSEWU
|
||||||
|
1811,18,KABUPATEN MESUJI
|
||||||
|
1812,18,KABUPATEN TULANG BAWANG BARAT
|
||||||
|
1813,18,KABUPATEN PESISIR BARAT
|
||||||
|
1871,18,KOTA BANDAR LAMPUNG
|
||||||
|
1872,18,KOTA METRO
|
||||||
|
1901,19,KABUPATEN BANGKA
|
||||||
|
1902,19,KABUPATEN BELITUNG
|
||||||
|
1903,19,KABUPATEN BANGKA BARAT
|
||||||
|
1904,19,KABUPATEN BANGKA TENGAH
|
||||||
|
1905,19,KABUPATEN BANGKA SELATAN
|
||||||
|
1906,19,KABUPATEN BELITUNG TIMUR
|
||||||
|
1971,19,KOTA PANGKAL PINANG
|
||||||
|
2101,21,KABUPATEN KARIMUN
|
||||||
|
2102,21,KABUPATEN BINTAN
|
||||||
|
2103,21,KABUPATEN NATUNA
|
||||||
|
2104,21,KABUPATEN LINGGA
|
||||||
|
2105,21,KABUPATEN KEPULAUAN ANAMBAS
|
||||||
|
2171,21,KOTA BATAM
|
||||||
|
2172,21,KOTA TANJUNG PINANG
|
||||||
|
3101,31,KABUPATEN KEPULAUAN SERIBU
|
||||||
|
3171,31,KOTA JAKARTA SELATAN
|
||||||
|
3172,31,KOTA JAKARTA TIMUR
|
||||||
|
3173,31,KOTA JAKARTA PUSAT
|
||||||
|
3174,31,KOTA JAKARTA BARAT
|
||||||
|
3175,31,KOTA JAKARTA UTARA
|
||||||
|
3201,32,KABUPATEN BOGOR
|
||||||
|
3202,32,KABUPATEN SUKABUMI
|
||||||
|
3203,32,KABUPATEN CIANJUR
|
||||||
|
3204,32,KABUPATEN BANDUNG
|
||||||
|
3205,32,KABUPATEN GARUT
|
||||||
|
3206,32,KABUPATEN TASIKMALAYA
|
||||||
|
3207,32,KABUPATEN CIAMIS
|
||||||
|
3208,32,KABUPATEN KUNINGAN
|
||||||
|
3209,32,KABUPATEN CIREBON
|
||||||
|
3210,32,KABUPATEN MAJALENGKA
|
||||||
|
3211,32,KABUPATEN SUMEDANG
|
||||||
|
3212,32,KABUPATEN INDRAMAYU
|
||||||
|
3213,32,KABUPATEN SUBANG
|
||||||
|
3214,32,KABUPATEN PURWAKARTA
|
||||||
|
3215,32,KABUPATEN KARAWANG
|
||||||
|
3216,32,KABUPATEN BEKASI
|
||||||
|
3217,32,KABUPATEN BANDUNG BARAT
|
||||||
|
3218,32,KABUPATEN PANGANDARAN
|
||||||
|
3271,32,KOTA BOGOR
|
||||||
|
3272,32,KOTA SUKABUMI
|
||||||
|
3273,32,KOTA BANDUNG
|
||||||
|
3274,32,KOTA CIREBON
|
||||||
|
3275,32,KOTA BEKASI
|
||||||
|
3276,32,KOTA DEPOK
|
||||||
|
3277,32,KOTA CIMAHI
|
||||||
|
3278,32,KOTA TASIKMALAYA
|
||||||
|
3279,32,KOTA BANJAR
|
||||||
|
3301,33,KABUPATEN CILACAP
|
||||||
|
3302,33,KABUPATEN BANYUMAS
|
||||||
|
3303,33,KABUPATEN PURBALINGGA
|
||||||
|
3304,33,KABUPATEN BANJARNEGARA
|
||||||
|
3305,33,KABUPATEN KEBUMEN
|
||||||
|
3306,33,KABUPATEN PURWOREJO
|
||||||
|
3307,33,KABUPATEN WONOSOBO
|
||||||
|
3308,33,KABUPATEN MAGELANG
|
||||||
|
3309,33,KABUPATEN BOYOLALI
|
||||||
|
3310,33,KABUPATEN KLATEN
|
||||||
|
3311,33,KABUPATEN SUKOHARJO
|
||||||
|
3312,33,KABUPATEN WONOGIRI
|
||||||
|
3313,33,KABUPATEN KARANGANYAR
|
||||||
|
3314,33,KABUPATEN SRAGEN
|
||||||
|
3315,33,KABUPATEN GROBOGAN
|
||||||
|
3316,33,KABUPATEN BLORA
|
||||||
|
3317,33,KABUPATEN REMBANG
|
||||||
|
3318,33,KABUPATEN PATI
|
||||||
|
3319,33,KABUPATEN KUDUS
|
||||||
|
3320,33,KABUPATEN JEPARA
|
||||||
|
3321,33,KABUPATEN DEMAK
|
||||||
|
3322,33,KABUPATEN SEMARANG
|
||||||
|
3323,33,KABUPATEN TEMANGGUNG
|
||||||
|
3324,33,KABUPATEN KENDAL
|
||||||
|
3325,33,KABUPATEN BATANG
|
||||||
|
3326,33,KABUPATEN PEKALONGAN
|
||||||
|
3327,33,KABUPATEN PEMALANG
|
||||||
|
3328,33,KABUPATEN TEGAL
|
||||||
|
3329,33,KABUPATEN BREBES
|
||||||
|
3371,33,KOTA MAGELANG
|
||||||
|
3372,33,KOTA SURAKARTA
|
||||||
|
3373,33,KOTA SALATIGA
|
||||||
|
3374,33,KOTA SEMARANG
|
||||||
|
3375,33,KOTA PEKALONGAN
|
||||||
|
3376,33,KOTA TEGAL
|
||||||
|
3401,34,KABUPATEN KULON PROGO
|
||||||
|
3402,34,KABUPATEN BANTUL
|
||||||
|
3403,34,KABUPATEN GUNUNG KIDUL
|
||||||
|
3404,34,KABUPATEN SLEMAN
|
||||||
|
3471,34,KOTA YOGYAKARTA
|
||||||
|
3501,35,KABUPATEN PACITAN
|
||||||
|
3502,35,KABUPATEN PONOROGO
|
||||||
|
3503,35,KABUPATEN TRENGGALEK
|
||||||
|
3504,35,KABUPATEN TULUNGAGUNG
|
||||||
|
3505,35,KABUPATEN BLITAR
|
||||||
|
3506,35,KABUPATEN KEDIRI
|
||||||
|
3507,35,KABUPATEN MALANG
|
||||||
|
3508,35,KABUPATEN LUMAJANG
|
||||||
|
3509,35,KABUPATEN JEMBER
|
||||||
|
3510,35,KABUPATEN BANYUWANGI
|
||||||
|
3511,35,KABUPATEN BONDOWOSO
|
||||||
|
3512,35,KABUPATEN SITUBONDO
|
||||||
|
3513,35,KABUPATEN PROBOLINGGO
|
||||||
|
3514,35,KABUPATEN PASURUAN
|
||||||
|
3515,35,KABUPATEN SIDOARJO
|
||||||
|
3516,35,KABUPATEN MOJOKERTO
|
||||||
|
3517,35,KABUPATEN JOMBANG
|
||||||
|
3518,35,KABUPATEN NGANJUK
|
||||||
|
3519,35,KABUPATEN MADIUN
|
||||||
|
3520,35,KABUPATEN MAGETAN
|
||||||
|
3521,35,KABUPATEN NGAWI
|
||||||
|
3522,35,KABUPATEN BOJONEGORO
|
||||||
|
3523,35,KABUPATEN TUBAN
|
||||||
|
3524,35,KABUPATEN LAMONGAN
|
||||||
|
3525,35,KABUPATEN GRESIK
|
||||||
|
3526,35,KABUPATEN BANGKALAN
|
||||||
|
3527,35,KABUPATEN SAMPANG
|
||||||
|
3528,35,KABUPATEN PAMEKASAN
|
||||||
|
3529,35,KABUPATEN SUMENEP
|
||||||
|
3571,35,KOTA KEDIRI
|
||||||
|
3572,35,KOTA BLITAR
|
||||||
|
3573,35,KOTA MALANG
|
||||||
|
3574,35,KOTA PROBOLINGGO
|
||||||
|
3575,35,KOTA PASURUAN
|
||||||
|
3576,35,KOTA MOJOKERTO
|
||||||
|
3577,35,KOTA MADIUN
|
||||||
|
3578,35,KOTA SURABAYA
|
||||||
|
3579,35,KOTA BATU
|
||||||
|
3601,36,KABUPATEN PANDEGLANG
|
||||||
|
3602,36,KABUPATEN LEBAK
|
||||||
|
3603,36,KABUPATEN TANGERANG
|
||||||
|
3604,36,KABUPATEN SERANG
|
||||||
|
3671,36,KOTA TANGERANG
|
||||||
|
3672,36,KOTA CILEGON
|
||||||
|
3673,36,KOTA SERANG
|
||||||
|
3674,36,KOTA TANGERANG SELATAN
|
||||||
|
5101,51,KABUPATEN JEMBRANA
|
||||||
|
5102,51,KABUPATEN TABANAN
|
||||||
|
5103,51,KABUPATEN BADUNG
|
||||||
|
5104,51,KABUPATEN GIANYAR
|
||||||
|
5105,51,KABUPATEN KLUNGKUNG
|
||||||
|
5106,51,KABUPATEN BANGLI
|
||||||
|
5107,51,KABUPATEN KARANG ASEM
|
||||||
|
5108,51,KABUPATEN BULELENG
|
||||||
|
5171,51,KOTA DENPASAR
|
||||||
|
5201,52,KABUPATEN LOMBOK BARAT
|
||||||
|
5202,52,KABUPATEN LOMBOK TENGAH
|
||||||
|
5203,52,KABUPATEN LOMBOK TIMUR
|
||||||
|
5204,52,KABUPATEN SUMBAWA
|
||||||
|
5205,52,KABUPATEN DOMPU
|
||||||
|
5206,52,KABUPATEN BIMA
|
||||||
|
5207,52,KABUPATEN SUMBAWA BARAT
|
||||||
|
5208,52,KABUPATEN LOMBOK UTARA
|
||||||
|
5271,52,KOTA MATARAM
|
||||||
|
5272,52,KOTA BIMA
|
||||||
|
5301,53,KABUPATEN SUMBA BARAT
|
||||||
|
5302,53,KABUPATEN SUMBA TIMUR
|
||||||
|
5303,53,KABUPATEN KUPANG
|
||||||
|
5304,53,KABUPATEN TIMOR TENGAH SELATAN
|
||||||
|
5305,53,KABUPATEN TIMOR TENGAH UTARA
|
||||||
|
5306,53,KABUPATEN BELU
|
||||||
|
5307,53,KABUPATEN ALOR
|
||||||
|
5308,53,KABUPATEN LEMBATA
|
||||||
|
5309,53,KABUPATEN FLORES TIMUR
|
||||||
|
5310,53,KABUPATEN SIKKA
|
||||||
|
5311,53,KABUPATEN ENDE
|
||||||
|
5312,53,KABUPATEN NGADA
|
||||||
|
5313,53,KABUPATEN MANGGARAI
|
||||||
|
5314,53,KABUPATEN ROTE NDAO
|
||||||
|
5315,53,KABUPATEN MANGGARAI BARAT
|
||||||
|
5316,53,KABUPATEN SUMBA TENGAH
|
||||||
|
5317,53,KABUPATEN SUMBA BARAT DAYA
|
||||||
|
5318,53,KABUPATEN NAGEKEO
|
||||||
|
5319,53,KABUPATEN MANGGARAI TIMUR
|
||||||
|
5320,53,KABUPATEN SABU RAIJUA
|
||||||
|
5321,53,KABUPATEN MALAKA
|
||||||
|
5371,53,KOTA KUPANG
|
||||||
|
6101,61,KABUPATEN SAMBAS
|
||||||
|
6102,61,KABUPATEN BENGKAYANG
|
||||||
|
6103,61,KABUPATEN LANDAK
|
||||||
|
6104,61,KABUPATEN MEMPAWAH
|
||||||
|
6105,61,KABUPATEN SANGGAU
|
||||||
|
6106,61,KABUPATEN KETAPANG
|
||||||
|
6107,61,KABUPATEN SINTANG
|
||||||
|
6108,61,KABUPATEN KAPUAS HULU
|
||||||
|
6109,61,KABUPATEN SEKADAU
|
||||||
|
6110,61,KABUPATEN MELAWI
|
||||||
|
6111,61,KABUPATEN KAYONG UTARA
|
||||||
|
6112,61,KABUPATEN KUBU RAYA
|
||||||
|
6171,61,KOTA PONTIANAK
|
||||||
|
6172,61,KOTA SINGKAWANG
|
||||||
|
6201,62,KABUPATEN KOTAWARINGIN BARAT
|
||||||
|
6202,62,KABUPATEN KOTAWARINGIN TIMUR
|
||||||
|
6203,62,KABUPATEN KAPUAS
|
||||||
|
6204,62,KABUPATEN BARITO SELATAN
|
||||||
|
6205,62,KABUPATEN BARITO UTARA
|
||||||
|
6206,62,KABUPATEN SUKAMARA
|
||||||
|
6207,62,KABUPATEN LAMANDAU
|
||||||
|
6208,62,KABUPATEN SERUYAN
|
||||||
|
6209,62,KABUPATEN KATINGAN
|
||||||
|
6210,62,KABUPATEN PULANG PISAU
|
||||||
|
6211,62,KABUPATEN GUNUNG MAS
|
||||||
|
6212,62,KABUPATEN BARITO TIMUR
|
||||||
|
6213,62,KABUPATEN MURUNG RAYA
|
||||||
|
6271,62,KOTA PALANGKA RAYA
|
||||||
|
6301,63,KABUPATEN TANAH LAUT
|
||||||
|
6302,63,KABUPATEN KOTA BARU
|
||||||
|
6303,63,KABUPATEN BANJAR
|
||||||
|
6304,63,KABUPATEN BARITO KUALA
|
||||||
|
6305,63,KABUPATEN TAPIN
|
||||||
|
6306,63,KABUPATEN HULU SUNGAI SELATAN
|
||||||
|
6307,63,KABUPATEN HULU SUNGAI TENGAH
|
||||||
|
6308,63,KABUPATEN HULU SUNGAI UTARA
|
||||||
|
6309,63,KABUPATEN TABALONG
|
||||||
|
6310,63,KABUPATEN TANAH BUMBU
|
||||||
|
6311,63,KABUPATEN BALANGAN
|
||||||
|
6371,63,KOTA BANJARMASIN
|
||||||
|
6372,63,KOTA BANJAR BARU
|
||||||
|
6401,64,KABUPATEN PASER
|
||||||
|
6402,64,KABUPATEN KUTAI BARAT
|
||||||
|
6403,64,KABUPATEN KUTAI KARTANEGARA
|
||||||
|
6404,64,KABUPATEN KUTAI TIMUR
|
||||||
|
6405,64,KABUPATEN BERAU
|
||||||
|
6409,64,KABUPATEN PENAJAM PASER UTARA
|
||||||
|
6411,64,KABUPATEN MAHAKAM HULU
|
||||||
|
6471,64,KOTA BALIKPAPAN
|
||||||
|
6472,64,KOTA SAMARINDA
|
||||||
|
6474,64,KOTA BONTANG
|
||||||
|
6501,65,KABUPATEN MALINAU
|
||||||
|
6502,65,KABUPATEN BULUNGAN
|
||||||
|
6503,65,KABUPATEN TANA TIDUNG
|
||||||
|
6504,65,KABUPATEN NUNUKAN
|
||||||
|
6571,65,KOTA TARAKAN
|
||||||
|
7101,71,KABUPATEN BOLAANG MONGONDOW
|
||||||
|
7102,71,KABUPATEN MINAHASA
|
||||||
|
7103,71,KABUPATEN KEPULAUAN SANGIHE
|
||||||
|
7104,71,KABUPATEN KEPULAUAN TALAUD
|
||||||
|
7105,71,KABUPATEN MINAHASA SELATAN
|
||||||
|
7106,71,KABUPATEN MINAHASA UTARA
|
||||||
|
7107,71,KABUPATEN BOLAANG MONGONDOW UTARA
|
||||||
|
7108,71,KABUPATEN SIAU TAGULANDANG BIARO
|
||||||
|
7109,71,KABUPATEN MINAHASA TENGGARA
|
||||||
|
7110,71,KABUPATEN BOLAANG MONGONDOW SELATAN
|
||||||
|
7111,71,KABUPATEN BOLAANG MONGONDOW TIMUR
|
||||||
|
7171,71,KOTA MANADO
|
||||||
|
7172,71,KOTA BITUNG
|
||||||
|
7173,71,KOTA TOMOHON
|
||||||
|
7174,71,KOTA KOTAMOBAGU
|
||||||
|
7201,72,KABUPATEN BANGGAI KEPULAUAN
|
||||||
|
7202,72,KABUPATEN BANGGAI
|
||||||
|
7203,72,KABUPATEN MOROWALI
|
||||||
|
7204,72,KABUPATEN POSO
|
||||||
|
7205,72,KABUPATEN DONGGALA
|
||||||
|
7206,72,KABUPATEN TOLI-TOLI
|
||||||
|
7207,72,KABUPATEN BUOL
|
||||||
|
7208,72,KABUPATEN PARIGI MOUTONG
|
||||||
|
7209,72,KABUPATEN TOJO UNA-UNA
|
||||||
|
7210,72,KABUPATEN SIGI
|
||||||
|
7211,72,KABUPATEN BANGGAI LAUT
|
||||||
|
7212,72,KABUPATEN MOROWALI UTARA
|
||||||
|
7271,72,KOTA PALU
|
||||||
|
7301,73,KABUPATEN KEPULAUAN SELAYAR
|
||||||
|
7302,73,KABUPATEN BULUKUMBA
|
||||||
|
7303,73,KABUPATEN BANTAENG
|
||||||
|
7304,73,KABUPATEN JENEPONTO
|
||||||
|
7305,73,KABUPATEN TAKALAR
|
||||||
|
7306,73,KABUPATEN GOWA
|
||||||
|
7307,73,KABUPATEN SINJAI
|
||||||
|
7308,73,KABUPATEN MAROS
|
||||||
|
7309,73,KABUPATEN PANGKAJENE DAN KEPULAUAN
|
||||||
|
7310,73,KABUPATEN BARRU
|
||||||
|
7311,73,KABUPATEN BONE
|
||||||
|
7312,73,KABUPATEN SOPPENG
|
||||||
|
7313,73,KABUPATEN WAJO
|
||||||
|
7314,73,KABUPATEN SIDENRENG RAPPANG
|
||||||
|
7315,73,KABUPATEN PINRANG
|
||||||
|
7316,73,KABUPATEN ENREKANG
|
||||||
|
7317,73,KABUPATEN LUWU
|
||||||
|
7318,73,KABUPATEN TANA TORAJA
|
||||||
|
7322,73,KABUPATEN LUWU UTARA
|
||||||
|
7325,73,KABUPATEN LUWU TIMUR
|
||||||
|
7326,73,KABUPATEN TORAJA UTARA
|
||||||
|
7371,73,KOTA MAKASSAR
|
||||||
|
7372,73,KOTA PAREPARE
|
||||||
|
7373,73,KOTA PALOPO
|
||||||
|
7401,74,KABUPATEN BUTON
|
||||||
|
7402,74,KABUPATEN MUNA
|
||||||
|
7403,74,KABUPATEN KONAWE
|
||||||
|
7404,74,KABUPATEN KOLAKA
|
||||||
|
7405,74,KABUPATEN KONAWE SELATAN
|
||||||
|
7406,74,KABUPATEN BOMBANA
|
||||||
|
7407,74,KABUPATEN WAKATOBI
|
||||||
|
7408,74,KABUPATEN KOLAKA UTARA
|
||||||
|
7409,74,KABUPATEN BUTON UTARA
|
||||||
|
7410,74,KABUPATEN KONAWE UTARA
|
||||||
|
7411,74,KABUPATEN KOLAKA TIMUR
|
||||||
|
7412,74,KABUPATEN KONAWE KEPULAUAN
|
||||||
|
7413,74,KABUPATEN MUNA BARAT
|
||||||
|
7414,74,KABUPATEN BUTON TENGAH
|
||||||
|
7415,74,KABUPATEN BUTON SELATAN
|
||||||
|
7471,74,KOTA KENDARI
|
||||||
|
7472,74,KOTA BAUBAU
|
||||||
|
7501,75,KABUPATEN BOALEMO
|
||||||
|
7502,75,KABUPATEN GORONTALO
|
||||||
|
7503,75,KABUPATEN POHUWATO
|
||||||
|
7504,75,KABUPATEN BONE BOLANGO
|
||||||
|
7505,75,KABUPATEN GORONTALO UTARA
|
||||||
|
7571,75,KOTA GORONTALO
|
||||||
|
7601,76,KABUPATEN MAJENE
|
||||||
|
7602,76,KABUPATEN POLEWALI MANDAR
|
||||||
|
7603,76,KABUPATEN MAMASA
|
||||||
|
7604,76,KABUPATEN MAMUJU
|
||||||
|
7605,76,KABUPATEN MAMUJU UTARA
|
||||||
|
7606,76,KABUPATEN MAMUJU TENGAH
|
||||||
|
8101,81,KABUPATEN MALUKU TENGGARA BARAT
|
||||||
|
8102,81,KABUPATEN MALUKU TENGGARA
|
||||||
|
8103,81,KABUPATEN MALUKU TENGAH
|
||||||
|
8104,81,KABUPATEN BURU
|
||||||
|
8105,81,KABUPATEN KEPULAUAN ARU
|
||||||
|
8106,81,KABUPATEN SERAM BAGIAN BARAT
|
||||||
|
8107,81,KABUPATEN SERAM BAGIAN TIMUR
|
||||||
|
8108,81,KABUPATEN MALUKU BARAT DAYA
|
||||||
|
8109,81,KABUPATEN BURU SELATAN
|
||||||
|
8171,81,KOTA AMBON
|
||||||
|
8172,81,KOTA TUAL
|
||||||
|
8201,82,KABUPATEN HALMAHERA BARAT
|
||||||
|
8202,82,KABUPATEN HALMAHERA TENGAH
|
||||||
|
8203,82,KABUPATEN KEPULAUAN SULA
|
||||||
|
8204,82,KABUPATEN HALMAHERA SELATAN
|
||||||
|
8205,82,KABUPATEN HALMAHERA UTARA
|
||||||
|
8206,82,KABUPATEN HALMAHERA TIMUR
|
||||||
|
8207,82,KABUPATEN PULAU MOROTAI
|
||||||
|
8208,82,KABUPATEN PULAU TALIABU
|
||||||
|
8271,82,KOTA TERNATE
|
||||||
|
8272,82,KOTA TIDORE KEPULAUAN
|
||||||
|
9101,91,KABUPATEN FAKFAK
|
||||||
|
9102,91,KABUPATEN KAIMANA
|
||||||
|
9103,91,KABUPATEN TELUK WONDAMA
|
||||||
|
9104,91,KABUPATEN TELUK BINTUNI
|
||||||
|
9105,91,KABUPATEN MANOKWARI
|
||||||
|
9106,91,KABUPATEN SORONG SELATAN
|
||||||
|
9107,91,KABUPATEN SORONG
|
||||||
|
9108,91,KABUPATEN RAJA AMPAT
|
||||||
|
9109,91,KABUPATEN TAMBRAUW
|
||||||
|
9110,91,KABUPATEN MAYBRAT
|
||||||
|
9111,91,KABUPATEN MANOKWARI SELATAN
|
||||||
|
9112,91,KABUPATEN PEGUNUNGAN ARFAK
|
||||||
|
9171,91,KOTA SORONG
|
||||||
|
9401,94,KABUPATEN MERAUKE
|
||||||
|
9402,94,KABUPATEN JAYAWIJAYA
|
||||||
|
9403,94,KABUPATEN JAYAPURA
|
||||||
|
9404,94,KABUPATEN NABIRE
|
||||||
|
9408,94,KABUPATEN KEPULAUAN YAPEN
|
||||||
|
9409,94,KABUPATEN BIAK NUMFOR
|
||||||
|
9410,94,KABUPATEN PANIAI
|
||||||
|
9411,94,KABUPATEN PUNCAK JAYA
|
||||||
|
9412,94,KABUPATEN MIMIKA
|
||||||
|
9413,94,KABUPATEN BOVEN DIGOEL
|
||||||
|
9414,94,KABUPATEN MAPPI
|
||||||
|
9415,94,KABUPATEN ASMAT
|
||||||
|
9416,94,KABUPATEN YAHUKIMO
|
||||||
|
9417,94,KABUPATEN PEGUNUNGAN BINTANG
|
||||||
|
9418,94,KABUPATEN TOLIKARA
|
||||||
|
9419,94,KABUPATEN SARMI
|
||||||
|
9420,94,KABUPATEN KEEROM
|
||||||
|
9426,94,KABUPATEN WAROPEN
|
||||||
|
9427,94,KABUPATEN SUPIORI
|
||||||
|
9428,94,KABUPATEN MAMBERAMO RAYA
|
||||||
|
9429,94,KABUPATEN NDUGA
|
||||||
|
9430,94,KABUPATEN LANNY JAYA
|
||||||
|
9431,94,KABUPATEN MAMBERAMO TENGAH
|
||||||
|
9432,94,KABUPATEN YALIMO
|
||||||
|
9433,94,KABUPATEN PUNCAK
|
||||||
|
9434,94,KABUPATEN DOGIYAI
|
||||||
|
9435,94,KABUPATEN INTAN JAYA
|
||||||
|
9436,94,KABUPATEN DEIYAI
|
||||||
|
9471,94,KOTA JAYAPURA
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Name,Status
|
||||||
|
SP001,DOKTER UMUM,true
|
||||||
|
SP002,DOKTER ANAK,true
|
||||||
|
SP003,DOKTER KANDUNGAN,true
|
||||||
|
SP004,DOKTER BEDAH,true
|
||||||
|
SP005,DOKTER JANTUNG,true
|
||||||
|
SP006,DOKTER SARAF,true
|
||||||
|
SP007,DOKTER THT,true
|
||||||
|
SP008,DOKTER MATA,true
|
||||||
|
SP009,DOKTER KULIT DAN KELAMIN,true
|
||||||
|
SP010,DOKTER GIGI,true
|
||||||
|
SP011,DOKTER PSIKIATRI,true
|
||||||
|
SP012,DOKTER ORTHOPEDI,true
|
||||||
|
SP013,DOKTER UROLOGI,true
|
||||||
|
SP014,DOKTER PARU,true
|
||||||
|
SP015,DOKTER GIZI,true
|
||||||
|
SP016,DOKTER REHABILITASI MEDIS,true
|
||||||
|
SP017,DOKTER GERIATRI,true
|
||||||
|
SP018,DOKTER ONKOLOGI,true
|
||||||
|
SP019,DOKTER KARDIOVASKULER,true
|
||||||
|
SP020,DOKTER NEFROLOGI,true
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Specialist_Code,Name,Status
|
||||||
|
SS001,SP001,MEDICINE,true
|
||||||
|
SS002,SP002,PEDIATRICS,true
|
||||||
|
SS003,SP003,OBSTETRICS,true
|
||||||
|
SS004,SP004,SURGERY,true
|
||||||
|
SS005,SP005,CARDIOLOGY,true
|
||||||
|
SS006,SP006,NEUROLOGY,true
|
||||||
|
SS007,SP007,OTORHINOLARYNGOLOGY,true
|
||||||
|
SS008,SP008,OPHTHALMOLOGY,true
|
||||||
|
SS009,SP009,DERMATOLOGY,true
|
||||||
|
SS010,SP010,DENTISTRY,true
|
||||||
|
SS011,SP011,PSYCHIATRY,true
|
||||||
|
SS012,SP012,ORTHOPEDICS,true
|
||||||
|
SS013,SP013,UROLOGY,true
|
||||||
|
SS014,SP014,PULMONOLOGY,true
|
||||||
|
SS015,SP015,NUTRITION,true
|
||||||
|
SS016,SP016,PHYSICAL MEDICINE,true
|
||||||
|
SS017,SP017,GERIATRICS,true
|
||||||
|
SS018,SP018,ONCOLOGY,true
|
||||||
|
SS019,SP019,CARDIOVASCULAR,true
|
||||||
|
SS020,SP020,NEPHROLOGY,true
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
Code,Name,Status
|
||||||
|
001,POLI UMUM,true
|
||||||
|
002,POLI ANAK,true
|
||||||
|
003,POLI KANDUNGAN,true
|
||||||
|
004,POLI BEDAH,true
|
||||||
|
005,POLI JANTUNG,true
|
||||||
|
006,POLI SARAF,true
|
||||||
|
007,POLI THT,true
|
||||||
|
008,POLI MATA,true
|
||||||
|
009,POLI KULIT,true
|
||||||
|
010,POLI GIGI,true
|
||||||
|
011,POLI PSIKIATRI,true
|
||||||
|
012,POLI ORTHOPEDI,true
|
||||||
|
013,POLI UROLOGI,true
|
||||||
|
014,POLI PARU,true
|
||||||
|
015,POLI GIZI,true
|
||||||
|
016,POLI REHABILITASI,true
|
||||||
|
017,POLI GERIATRI,true
|
||||||
|
018,POLI ONKOLOGI,true
|
||||||
|
019,POLI KARDIOVASKULER,true
|
||||||
|
020,POLI NEFROLOGI,true
|
||||||
|
+80535
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
|||||||
|
// internal/infrastructure/database/database.go
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-general/internal/infrastructure/config"
|
||||||
|
"service-general/pkg/logger" // Import logger Anda
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/stdlib"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq" // PASTIKAN IMPORT INI ADA
|
||||||
|
|
||||||
|
// Driver GORM di-import secara eksplisit untuk Migration
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/driver/sqlserver"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger" // Import logger dari GORM
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ... (tipe DatabaseType dan interface Service tetap sama)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
// GORM DB adalah sumber utama
|
||||||
|
gormDatabases map[string]*gorm.DB
|
||||||
|
// Map untuk menyimpan koneksi mentah yang diekstrak dari GORM
|
||||||
|
sqlDatabases map[string]*sql.DB
|
||||||
|
sqlxDatabases map[string]*sqlx.DB
|
||||||
|
mongoClients map[string]*mongo.Client
|
||||||
|
readReplicas map[string][]*sql.DB
|
||||||
|
configs map[string]config.DatabaseConfig
|
||||||
|
readConfigs map[string][]config.DatabaseConfig
|
||||||
|
mu sync.RWMutex
|
||||||
|
readBalancer map[string]int
|
||||||
|
// PERBAIKAN 1: Kembali ke tipe yang benar untuk listener PostgreSQL
|
||||||
|
listeners map[string]*pq.Listener
|
||||||
|
listenersMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbManager *service
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// ... (fungsi New dan loadFromConfig tetap sama)
|
||||||
|
|
||||||
|
// --- PERBAIKAN 2: Buat Adapter untuk Logger GORM ---
|
||||||
|
|
||||||
|
// gormLoggerAdapter adalah adapter untuk menghubungkan logger kita dengan GORM
|
||||||
|
type gormLoggerAdapter struct {
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGormLoggerAdapter membuat instance baru dari adapter
|
||||||
|
func NewGormLoggerAdapter(l logger.Logger) logger.Interface {
|
||||||
|
// Tentukan level log GORM berdasarkan level logger kita
|
||||||
|
// Ini adalah implementasi sederhana, Anda bisa membuatnya lebih dinamis
|
||||||
|
logLevel := logger.Silent
|
||||||
|
if l != nil {
|
||||||
|
// Anda bisa menambahkan logika untuk menentukan level dari logger Anda
|
||||||
|
// Untuk sekarang, kita hardcode ke Info
|
||||||
|
logLevel = logger.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gormLoggerAdapter{
|
||||||
|
logger: l,
|
||||||
|
// Konfigurasi logger GORM
|
||||||
|
Config: logger.Config{
|
||||||
|
SlowThreshold: 200 * time.Millisecond,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
IgnoreRecordNotFoundError: false,
|
||||||
|
Colorful: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogMode menetapkan level log
|
||||||
|
func (l *gormLoggerAdapter) LogMode(level logger.LogLevel) logger.Interface {
|
||||||
|
newLogger := *l
|
||||||
|
newLogger.Config.LogLevel = level
|
||||||
|
return &newLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info mencatat log info
|
||||||
|
func (l *gormLoggerAdapter) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
l.logger.WithContext(ctx).Info(fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn mencatat log warning
|
||||||
|
func (l *gormLoggerAdapter) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
l.logger.WithContext(ctx).Warn(fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error mencatat log error
|
||||||
|
func (l *gormLoggerAdapter) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
l.logger.WithContext(ctx).Error(fmt.Sprintf(msg, data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace mencatat log trace (SQL query)
|
||||||
|
func (l *gormLoggerAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
sql, rows := fc()
|
||||||
|
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"duration": elapsed,
|
||||||
|
"rows": rows,
|
||||||
|
"sql": sql,
|
||||||
|
"error": err,
|
||||||
|
"function": "database.trace",
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err != nil && l.Config.LogLevel >= logger.Error:
|
||||||
|
l.logger.WithContext(ctx).WithFields(logger.Any("error", err)).Error("Database error", logger.Any("details", fields))
|
||||||
|
case elapsed > l.Config.SlowThreshold && l.Config.LogLevel >= logger.Warn:
|
||||||
|
l.logger.WithContext(ctx).Warn("Slow database query", logger.Any("details", fields))
|
||||||
|
case l.Config.LogLevel == logger.Info:
|
||||||
|
l.logger.WithContext(ctx).Debug("Database query", logger.Any("details", fields))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Metode Koneksi GORM (diperbaiki untuk menggunakan adapter) ---
|
||||||
|
|
||||||
|
func (s *service) openPostgresGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=Asia/Jakarta",
|
||||||
|
config.Host, config.Username, config.Password, config.Database, config.Port, config.SSLMode)
|
||||||
|
|
||||||
|
if config.Schema != "" {
|
||||||
|
dsn += fmt.Sprintf(" search_path=%s", config.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERBAIKAN: Gunakan adapter logger kita
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RequireSSL {
|
||||||
|
// ... (konfigurasi SSL tetap sama)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gorm.Open(postgres.Open(dsn), gormConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) openMySQLGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||||
|
|
||||||
|
if config.RequireSSL {
|
||||||
|
dsn += "&tls=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERBAIKAN: Gunakan adapter logger kita
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return gorm.Open(mysql.Open(dsn), gormConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) openSQLServerGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s",
|
||||||
|
config.Username, config.Password, config.Host, config.Port, config.Database)
|
||||||
|
|
||||||
|
if config.RequireSSL {
|
||||||
|
// ... (konfigurasi SSL tetap sama)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERBAIKAN: Gunakan adapter logger kita
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return gorm.Open(sqlserver.Open(dsn), gormConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) openSQLiteGORM(config config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
// PERBAIKAN: Gunakan adapter logger kita
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: NewGormLoggerAdapter(logger.Default()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return gorm.Open(sqlite.Open(config.Path), gormConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Metode Lainnya ---
|
||||||
|
|
||||||
|
// ... (semua metode lain seperti GetGormDB, GetDB, Migrate, Health, Close, dll. tidak perlu diubah)
|
||||||
|
// ... (kecuali mungkin bagian Close yang membersihkan listeners)
|
||||||
|
|
||||||
|
// Di fungsi Close, pastikan tipe listener sudah benar
|
||||||
|
func (s *service) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
for name, listener := range s.listeners {
|
||||||
|
if err := listener.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close listener for %s: %w", name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ... (sisa kode Close)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Di fungsi ListenForChanges, pastikan Anda menggunakan pq.NewListener
|
||||||
|
func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error {
|
||||||
|
// ... (kode Anda sudah benar menggunakan pq.NewListener)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
h1:w6tFa/NMDQdQDXl91m3KqSaPSa9ktGQH7la07Gu6eoc=
|
||||||
|
20250904105930.sql h1:MEM6blCgke9DzWQSTnLzasbPIrcHssNNrJqZpSkEo6k=
|
||||||
|
20250904141448.sql h1:J8cmYNk4ZrG9fhfbi2Z1IWz7YkfvhFqTzrLFo58BPY0=
|
||||||
|
20250908062237.sql h1:Pu23yEW/aKkwozHoOuROvHS/GK4ngARJGdO7FB7HZuI=
|
||||||
|
20250908062323.sql h1:oXl6Z143tOpIl4EfP4B8JNU8LrMvVmHEtCgAfiB4gs8=
|
||||||
|
20250908073811.sql h1:m2aNXfnGxnLq1+rVWrh4f60q7fhyhV3gEwNu/OIqQlE=
|
||||||
|
20250908073839.sql h1:cPk54xjLdMs26uY8ZHjNWLuyfAMzV7Zb0/9oJQrsw04=
|
||||||
|
20250910055902.sql h1:5xwjAV6QbtZT9empTJKfhyAjdknbHzb15B0Ku5dzqtQ=
|
||||||
|
20250915123412.sql h1:D83xaU2YlDEd21HLup/YQpQ2easMToYCyy/oK6AFgQs=
|
||||||
|
20250916043819.sql h1:ekoTJsBqQZ8G8n0qJ03d13+eoNoc7sAUEQGA5D/CCxk=
|
||||||
|
20250917040616.sql h1:zoCnmcXuM7AVv85SmN7RmFglCgJnoDmpRWExH0LAc9Q=
|
||||||
|
20250917040751.sql h1:J1xyRrh32y1+lezwAyNwPcUQ6ABBSgbvzNLva4SVdQU=
|
||||||
|
20250917045138.sql h1:jKe1Z0uOLG4SGBYM+S/3P+/zMPztmgoderD5swnMuCg=
|
||||||
|
20250917093645.sql h1:cNI3Pbz1R3LxvIXLuexafJFCXUXrmuFCgXXJ2sG+FW0=
|
||||||
|
20250918073552.sql h1:RJ1SvMzP6aeWnoPVD3eVAmIQOkcp6Php8z3QRri6v4g=
|
||||||
|
20250918073742.sql h1:+cEsnJTJFybe2fR69ZoOiX2R6c6iITl4m6WTZ1hjyzY=
|
||||||
|
20250918074745.sql h1:2hNVQCXF/dVYXAh+T/7oBFgERGWxzVb2FXJjwkFWGCI=
|
||||||
|
20250923025134.sql h1:Ykz/qpHiGDXPsCsWTjydQFVSibZP2D+h2fIeb2h2JGA=
|
||||||
|
20250924051317.sql h1:yQuW6SwJxIOM5fcxeAaie5lSm1oLysU/C2hH2xNCVoQ=
|
||||||
|
20250929034321.sql h1:101FJ8VH12mrZWlt/X1gvKUGOhoiF8tFbjiapAjnHzg=
|
||||||
|
20250929034428.sql h1:i+pROD9p+g5dOmmZma6WF/0Hw5g3Ha28NN85iTo1K34=
|
||||||
|
20250930025550.sql h1:+F+CsCUXD/ql0tHGEow70GhPBX1ZybVn+bh/T4YMh7Y=
|
||||||
|
20250930140351.sql h1:9AAEG1AnOAH+o0+oHL5G7I8vqlWOhwRlCGyyCpT/y1Q=
|
||||||
|
20251002085604.sql h1:3xZ68eYp4urXRnvotNH1XvG2mYOSDV/j3zHEZ/txg5E=
|
||||||
|
20251003032030.sql h1:HB+mQ2lXMNomHDpaRhB/9IwYI9/YiDO5eOJ+nAQH/jw=
|
||||||
|
20251005060450.sql h1:LbtCE2b+8osM3CvnmQJH1uCPtn+d7WchsslBOz8bL3Q=
|
||||||
|
20251006041122.sql h1:MlS7f21z06sutnf9dIekt5fuHJr4lgcQ4uCuCXAGsfc=
|
||||||
|
20251006045658.sql h1:3FmGCPCzjgMPdWDRodZTsx3KVaodd9zB9ilib69aewk=
|
||||||
|
20251006045928.sql h1:Z5g31PmnzNwk/OKdODcxZGm8fjJQdMFK32Xfnt3bRHg=
|
||||||
|
20251007022859.sql h1:FO03zEfaNEk/aXwY81d5Lp3MoBB9kPQuXlXJ4BPiSR8=
|
||||||
|
20251008031337.sql h1:l+sxUAGvcTfj3I6kAFHo+T6AYodC9k9GkR+jaKO2xXc=
|
||||||
|
20251008031554.sql h1:AqrVfIhSzY3PCy8ZlP5W91wn2iznfIuj5qQfubp6/94=
|
||||||
|
20251008052346.sql h1:nxnXmooIJ6r1mmzwnw+6efxLfc/k9h2aE6RMptPRons=
|
||||||
|
20251008073620.sql h1:6YsJp1W4SmQJ1lxpqF27BBlDC1zqhw7Yhc7pLzQTY6M=
|
||||||
|
20251009042854.sql h1:nkBV+R6j0fg7/JY6wH3eb5Vv0asJLnXmb6lINfT/GLQ=
|
||||||
|
20251009052657.sql h1:EPvdsib5rzCGPryd10HShGKvFPwM/R5S2lIVwtYxpms=
|
||||||
|
20251010031743.sql h1:T8IZmx8/btRFKLzTe78MzcBsPJNodnLvB0tby9QkirQ=
|
||||||
|
20251010070721.sql h1:5NQUk/yOV6sABLCB7swx++YIOyJe6MnU+yt1nRzde5w=
|
||||||
|
20251010072711.sql h1:ZJNqR2piyu8xJhBvVABSlnGEoKSKae3wuEs+wshPe4k=
|
||||||
|
20251013044536.sql h1:0Xjw8fNILiT8nnfrJDZgQnPf3dntmIoilbapnih8AE4=
|
||||||
|
20251013051438.sql h1:lfSuw5mgJnePBJamvhZ81osFIouXeiIEiSZ/evdwo48=
|
||||||
|
20251013081808.sql h1:ijgjNX08G6GBjA/ks8EKtb7P7Y7Cg7zbhqEOruGnv6M=
|
||||||
|
20251014060047.sql h1:0jqj49WTtneEIMQDBoo4c095ZGi8sCrA8NnHBrPU6D8=
|
||||||
|
20251014063537.sql h1:VZLXol0PTsTW21Epg6vBPsztWkDtcxup9F/z88EGgIg=
|
||||||
|
20251014063720.sql h1:2HVUyCV0ud3BJJDH2GEKZN/+IWLFPCsN1KqhP6csO14=
|
||||||
|
20251015045455.sql h1:MeLWmMhAOAz8b15Dd7IAQnt6JxjSml02XCXK22C0Lpg=
|
||||||
|
20251016010845.sql h1:4BncQdDOasRZJkzVJrSJJA7091A9VPNVx/faUCUPhBM=
|
||||||
|
20251016011023.sql h1:9JB9eFZKURK5RoCVDKR6glSvdJ8NTXrN7K/4q51zkz4=
|
||||||
|
20251016062912.sql h1:ACNn0fe+EMqUt3hoY+Dr3uqAV/QICBa1+mIW7fUc9Fk=
|
||||||
|
20251017060617.sql h1:4T3t9ifWrEQTPMSM0XJ98pF7Qdt+UfgtMui17bhrnWI=
|
||||||
|
20251017082207.sql h1:8vLG1l/saRRMHXkyA4nelJyjaSddhZd6r7R+Uo4JS/c=
|
||||||
|
20251018032635.sql h1:2xey5gnO3y2XSOrU8MLlIfoylPKbRGDRtHDD07B3MbQ=
|
||||||
|
20251018040322.sql h1:k/pdNiSoT8zFPqNQ/avOD0vYkNh3BTD64IlHrfVXr7I=
|
||||||
|
20251019093915.sql h1:hFcQE0y+p5dZiVwePGsRGto9m/q6kJNiUZbVDd5Rnjk=
|
||||||
|
20251020062553.sql h1:Iw7hulcm5iRQlfW+ygA4iTPxLqkxx6h9vXMXEwUAHKs=
|
||||||
|
20251021041042.sql h1:wMgSivBV2A0NDcsLmKGIp0kMcVh2IODSG9b4dgzCaOM=
|
||||||
|
20251021075552.sql h1:8gfSMAglflNO6L0sSzxFNEubYN8/O4thT7OQT+WH+3M=
|
||||||
|
20251023044432.sql h1:MkvajJs3bfk9+wHvQ43/ccAluJEBARm1gWr1u92ccLA=
|
||||||
|
20251024034832.sql h1:x3s3VEVYLOSKLAFxJGb2+c1FyTMMvPE+9k4Ew7rKQaI=
|
||||||
|
20251024074315.sql h1:EjAjelgi5qAfcRq/8vPTlGGYHvAKxNTllm8f0SzZDns=
|
||||||
|
20251025013451.sql h1:6hnuIiwYiG+6nLhOY/+Yyn+I6ZCFNRZxrJNqBV6HLqE=
|
||||||
|
20251025013609.sql h1:evPJaTD8WxYRMOJZHkSr7ONLx9PYxT+ankzQt9c/sJ0=
|
||||||
|
20251027075128.sql h1:/iFQBM1sytjqpyQSOx61q33gnorMgxTiFVSuL6bQqsM=
|
||||||
|
20251027091406.sql h1:eCZGtUkxAzEAqpC9UsGpP8Df9mS0DEOqSl885LgqpvM=
|
||||||
|
20251102002037.sql h1:lFJbuoZ2LMQnUNGdcwHVY3Xlfslgzu9t2WByT8yfOZI=
|
||||||
|
20251102091932.sql h1:rmdhb5m+P+fU8jROBZNyeYgZKuQvucsuljXv4ZVzvks=
|
||||||
|
20251103081637.sql h1:tf3BcwTeIw+oxMEisKDDfyKnBfalTLs8b0PJA8JWYxY=
|
||||||
|
20251104042334.sql h1:7PDMWOhmJywolAPKFZ14XaDBeMvcxShaXFN2IemNtzk=
|
||||||
|
20251104043530.sql h1:qvYVp3ysPf27f1BcoRNCFGovxuVE12lg9d6Xzda6zWU=
|
||||||
|
20251104080952.sql h1:avghpv1n3yaCDR/TA0X+hgxDGoLBQGu/GJUwj4VT/Ic=
|
||||||
|
20251104084135.sql h1:rg+eRE5/5sYWR7z+Xyn0zKw8rr8P/oWxF0xhcNVnNec=
|
||||||
|
20251105044629.sql h1:4NU27HeKUNFsV82LacnwmnCSAH0pSbZR9J9/ZESRs6M=
|
||||||
|
20251105121808.sql h1:fii6LjqWYjrm/pEIqttfvJI6QEUL49gque8wYHh1+yI=
|
||||||
|
20251106035305.sql h1:oQ7BwnxPuwY2q98adIVc+lNwL/Sz1OceLJeClDo9/TI=
|
||||||
|
20251106040137.sql h1:ppcqkVoT0o9jZcjI/TN7LuaPxXhJQhnIXEJtloP/46o=
|
||||||
|
20251106041333.sql h1:2JkxyelQ/EeB+boL5bfpnzefw32ttEGKvKchtQjWmAU=
|
||||||
|
20251106042006.sql h1:ruppYa1kAJQUU3ufQBbKGMcXrGbGJJiRPclT+dNc/YQ=
|
||||||
|
20251106050412.sql h1:MiEMJ1HCFYnalKuq3Z38xJeogfBAMqsTv2sG4EF8dDw=
|
||||||
|
20251106063418.sql h1:y3veDJPjKekOWLCZek/LgQwXPRhZtOppTfUXiqoL95s=
|
||||||
|
20251106071906.sql h1:/TUZA3XpMY23qEJXdkTwlzrNMvSSl6JJniPcgAttBaw=
|
||||||
|
20251106073157.sql h1:78txeibJ602DMD7huD618ZSMt6phSRzDNPTlo0PGyrc=
|
||||||
|
20251106074218.sql h1:8Xz7WywrtUnSxOHhlal53gG9rE7r86LFUt5zBFe/mIs=
|
||||||
|
20251106081846.sql h1:jp91Bf5bxGXMiUB1VIuN6y768vb2iWwow44WfCE5J5k=
|
||||||
|
20251106082844.sql h1:RHYzRO4G1fSWwf+xc/3QezZ/Iil67cZPIgNpNz3TNhQ=
|
||||||
|
20251106090021.sql h1:dFDk6mq+zjbYWmfWIrHf9DiKvvoXHjrr0++zssMTWP8=
|
||||||
|
20251106144745.sql h1:aHcr23iBFqCHer5D/SsPMXBCLjGqUYvWYfRU8jSJgIw=
|
||||||
|
20251107012049.sql h1:hu/7NHhnAkT4xK0RNtqmMDdH1Bo5EZbl7itDRjiCT+g=
|
||||||
|
20251107064812.sql h1:sfCXDQYnMf0ddrQ9oYljWJLLSt9NJjJV6o8VS3p7aZE=
|
||||||
|
20251107064937.sql h1:DlYGJ9LZFwZyR7jBP5zaGB128aIc4HAixBKPYCz9EkY=
|
||||||
|
20251107071420.sql h1:ynCdZAd2utLl+FhtWZwtahNXgIVOvuk3s/rOq7lfXA4=
|
||||||
|
20251107074318.sql h1:WE9cPhibWtZ0dbu1VEGirTeY6ijFYGMNhHdBtM32kOc=
|
||||||
|
20251107075050.sql h1:8tvneruqdynDOaJK1+0z4CH7YXZStZpGdqwIeOMLik4=
|
||||||
|
20251107080604.sql h1:8c4jd4Tql7tcdhbI9NS0tgvN+ADu9FnCf8wMUbmW7A0=
|
||||||
|
20251107081830.sql h1:SAAe3lmsm9vGXuSEsDdl7ad0EAxP5CMmFRDEgp9M7yY=
|
||||||
|
20251107091033.sql h1:JLdX/u7GUdBfjrPrMSNAqc8HtSoj0YA9iW9Vc6FJZdw=
|
||||||
|
20251107091209.sql h1:CzhYtwAwT+GHrbqcagnJE+v3mbl/rObf1IJaLCKlzrs=
|
||||||
|
20251107091541.sql h1:+3ZyWJTftDY2JeWThXuIxGWpUBnyMPyOyY4jBjdWYJI=
|
||||||
|
20251110012217.sql h1:f4Z8TuGc+XMSJ+Ekn4/PeHRE2FlHWkc5gKPJB0hAX7c=
|
||||||
|
20251110012306.sql h1:ENPyI6Kjdk6qKJQb0yJ6MCTDPAmO1WD/uhKuCSl+jYo=
|
||||||
|
20251110052049.sql h1:OrQ0acnyoQLKnTitZfnBcVr5jDslF59OFLaqT7SpdVs=
|
||||||
|
20251110062042.sql h1:9KwldQt0NpVPR86L0T4hlkfHAGau+7CiZYgu5rF+yhg=
|
||||||
|
20251110063202.sql h1:A117DuZmZ6U0jWHA3DISnr+yvBjKIr1ObrUr047YezQ=
|
||||||
|
20251110063633.sql h1:qTiC0F19JnhUIXF4LGJQ21jEV6kKGyhTr1x2kimFqPQ=
|
||||||
|
20251110085551.sql h1:HZcJM0RSC6HBaUSjKBE8MgDG8Vn9f3LmwA/OnT9Cp7I=
|
||||||
|
20251110091516.sql h1:W3AQhQLgirEWuCObbLl+Prdrbq6k6EEY1xcoWsmbog4=
|
||||||
|
20251110091948.sql h1:3tsITMrZr/T+L4wqUMz8sHS229jCJl4T0Nu3dMccxH8=
|
||||||
|
20251110092729.sql h1:uU+k88RH/e0Ns4/SmJl03RVYPscBAPuiLfxR6CJqaf0=
|
||||||
|
20251110093522.sql h1:O7upSj8VNjzvroL4IU59bfxKATOkAVGBArcUbVNq9aM=
|
||||||
|
20251110100258.sql h1://JSArUMNI3/gAtYDx2VN94C198SFW0ANjgs+p6eCRM=
|
||||||
|
20251110100545.sql h1:ENPOqeJYRpMI4e8VCKwaQgaql8se6pIidAhG2cjskBg=
|
||||||
|
20251110155448.sql h1:VW9KE0jxWy4flZ28sdXmhY6JWd6eKAX/cVL8KWRVii4=
|
||||||
|
20251111072601.sql h1:99WWzGjcDDFNC2cHRfPu4MBLWNrgPMY5HAYUbmIcVRA=
|
||||||
|
20251111073546.sql h1:iIGwFNfUgStdczw/PXTH3SD84xAyxrbZICofc3M8TMk=
|
||||||
|
20251111074148.sql h1:mzkezSKwCV2bTZ/0BHiTCX5qAy4uHpwar1xzwAH15Ko=
|
||||||
|
20251111074652.sql h1:lYh4/3BHV24pgPC0pP4RUKb+XtL/6AsZGPItRpnzBiQ=
|
||||||
|
20251111082257.sql h1:+cIZ1lf8HYGSL6t6U88plLKk8nOO1YVdV7h3YOM84ds=
|
||||||
|
20251111111017.sql h1:xzlhR2xLfOj4ddYlesS+bpEeDrxiAf5ILNOspsbZjBU=
|
||||||
|
20251113101344.sql h1:vSzThY3p6XYTj0dJ2uFVkHmlytyrFAnGE58CJLx364I=
|
||||||
|
20251113120533.sql h1:lfjgdqRGo/EohrVw8sWlFbDjh3ASTsfQ6pr3qjafqvc=
|
||||||
|
20251114062746.sql h1:6DLYjfJ60PkAABZTXvOwSE+xrU25oyoX7gpYlBnA9cw=
|
||||||
|
20251117005942.sql h1:0XoJKca8IOaB9QKFVF/qPY7jKcIgh6m/LODQuE06SAs=
|
||||||
|
20251117075427.sql h1:LhY2urosfoSu1/vkHmgsNP4JA4DuWBs9gL59yMqcF8M=
|
||||||
|
20251118074929.sql h1:3RWBD6BziQVw6WSfthgoVuhTELHER9NrIuZm4hY/F1A=
|
||||||
|
20251119063438.sql h1:EVTG3ZrHjCmM95YPASZTRPiwHrG6e33A2vO4hLSAaBQ=
|
||||||
|
20251119065730.sql h1:s98wnZOCW6NbnwDS3H53XIQ60u6B9bDwBLNvy5+Rn64=
|
||||||
|
20251119072302.sql h1:ZL+VHekLYqgNtOFKlj02WHrAk11dtTxQkmyTKE8IOzY=
|
||||||
|
20251119072450.sql h1:SiJy2vpSvoPFw4J1SW7HjGSJzrqR62NsjRYAeabV+kc=
|
||||||
|
20251120005512.sql h1:hJUaWXYJ3HiSquBTw8OKhymjA4O43ehAMGfiOY8W87Q=
|
||||||
|
20251120074415.sql h1:dNmcqZHqfZwi/wtYvO/pSFgImN2scXL0p/7g0+HLp0s=
|
||||||
|
20251121033803.sql h1:onlddYGo+tQdGaEdJPqdsx3sncGnwEvqMSCksF6vjjI=
|
||||||
|
20251124071457.sql h1:ikQLIXFikUbkUd55uJBmEvqLGEC9t0db+Om4TQsWP7M=
|
||||||
|
20251125125303.sql h1:OLKbNPO36AHtIDursW9AeBvf60sahQKC1iOjHmpx0MA=
|
||||||
|
20251126064057.sql h1:6al3PTWbw/WGiBcrRrVWppOMLtG+eRaH/qSLbnmh1kQ=
|
||||||
|
20251201081333.sql h1:dD+jcisoUYnxRYWdtVcnxbDeYjUW12/R/qarYQr+utg=
|
||||||
|
20251201104439.sql h1:h4tz5uetbCKeOI3agSVmAeh6jUfECYy0LhQ7HooYhzg=
|
||||||
|
20251201113804.sql h1:DdUzqfLdy4AUpVSRFDrIJXntGmtKgZrtvv8MAT4mMxs=
|
||||||
|
20251201113858.sql h1:XSpicm4aWjrfeY5EYIiw8Ios9v7BGfzZJxpW/57Qwqs=
|
||||||
|
20251201114751.sql h1:qx+ShdHLacVFcRwwGrW1zMKFHOjMGdnusk7CEfyV8ig=
|
||||||
|
20251201114913.sql h1:6l/m2/6fmTIjFmKio0QmB8oTwBDCHTIkbuYTODQ7Gwk=
|
||||||
|
20251202030445.sql h1:5+6ss5KdN6pll8Qtf0RGGyy/BURNO3dBgIBJOzDb624=
|
||||||
|
20251202044430.sql h1:nJtlQDGj7ZJtJ07NYcF41JCn9ck50GyDPoitTOe8P/s=
|
||||||
|
20251202063820.sql h1:dXGXAp2Aq1PUuwAT5FtBquZUeEv5Pdb81/bFt+KR274=
|
||||||
|
20251202064000.sql h1:+W5j/AipU7qnFLCTaS7jVd/SIbIPo4k5rRScAvu0L44=
|
||||||
|
20251202130629.sql h1:rB/+hjwgQnHoA/uOv4pJbB0iXAeaPMinlzRI/755raY=
|
||||||
|
20251202160848.sql h1:KbTqHicChkfszdW9pQzoPlnvtECKDPmwpV3Rc7+5a8o=
|
||||||
|
20251202180207.sql h1:4DMAex+Go7MuMt15Di/J1C/ag15KwgWW1u8KDFmbJ9M=
|
||||||
|
20251202231005.sql h1:4GcnNRFZnjY1nASBj1IUkzLYCYIfnSE9s6kcWXZSiO8=
|
||||||
|
20251203205052.sql h1:0TZ4D3YKO2lmOAa60nhkJAQSx8QHgd+Zj4R39bPdU8Y=
|
||||||
|
20251205073858.sql h1:F4o2SDJ/deNjvKaimBGccZ9t4Fir01YocT6emhEoqiU=
|
||||||
|
20251205211957.sql h1:rJLemdhIo7NQRgsl5DANVuz8pAee7HciyGVET7V/DMk=
|
||||||
|
20251205214433.sql h1:1axU9bZaXW+sASNXQn08dKBX+fDEj1qHGiigEoJQXjY=
|
||||||
|
20251205221124.sql h1:TEy3NdLtM8+BkHwVT11kKvX3V28PP1bDMocZ0404Tm4=
|
||||||
|
20251206021053.sql h1:cEmwbMKPz8DY2OUsr3ym3blyp823418qMnx/AJFFYqY=
|
||||||
|
20251207020537.sql h1:hCENQuZ6jXSp3LrmZ7FgLxvI1gJNgBa+F39/RosKfgk=
|
||||||
|
20251207212015.sql h1:uhNmVl9qXZnqwy3GXfxh5JJjjXK4mztmL19ioezZwVQ=
|
||||||
|
20251207221222.sql h1:QMxCwIwb4M9jA9z8BJ9aNsOcMVwuZ1ZSsx0FjOlQjek=
|
||||||
|
20251209022744.sql h1:BBb5VV+hUGtKW+nZhu/C6OhBgiQhs84vMYfLM51V+VA=
|
||||||
|
20251209025908.sql h1:42z/3wPoOt/nkS92xZ9PHeu7m0IWgVICETkf6Ugtx7w=
|
||||||
|
20251209030538.sql h1:M2zy92ZTBTHLzbKs7cv1S5epLbJPQPVxME5E8nrPL2w=
|
||||||
|
20251209051742.sql h1:AKCu0xgzUlH5MjjBQLiHq6Gw54HA0j++Eknj1i6FHAw=
|
||||||
|
20251209064304.sql h1:MSMSfF2HVU7Qg/nyHmh89i7+oAj3V2wbPv/5AQcYNYM=
|
||||||
|
20251209070128.sql h1:CU3U33jEhzNFa2yc7aBGHv/XcyFEHRkjHUJFLTKZRjc=
|
||||||
|
20251209084929.sql h1:5H1z+0hOmDHQastabHLaxN5d6YWiD2p4+nUf1Fmthrc=
|
||||||
|
20251210145148.sql h1:tAs3iNEHnASYEQ4/h8XDXmKPvVASgfP6oTjnFdmBqFA=
|
||||||
|
20251211101547.sql h1:hGxvjf2XLoHJIQQlCoSOgqOrIkGjk1A7CLdDV1R4pgc=
|
||||||
|
20251211113942.sql h1:uxNunwWePW7UpR+8KtdvqQ4mUofJascWOOUEyRCKIEI=
|
||||||
|
20251216074834.sql h1:jja2bz2dtaC4327V2iqs9LP7MsW6ycz68BM0KPld9xs=
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
|||||||
|
# Flexible Database Seeder
|
||||||
|
|
||||||
|
A flexible and extensible database seeder for Go applications with GORM support.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎯 **Flexible Configuration**: Support for any table structure
|
||||||
|
- 📊 **CSV Import**: Import data from CSV files with customizable mapping
|
||||||
|
- 🔄 **Batch Processing**: Efficient bulk inserts with configurable batch sizes
|
||||||
|
- 🧪 **Dry Run Mode**: Preview seeding without actual database changes
|
||||||
|
- ✅ **Validation**: Validate CSV files and configurations before seeding
|
||||||
|
- 📝 **Column Mapping**: Map CSV columns to struct fields flexibly
|
||||||
|
- 🗂️ **Multiple Tables**: Support for all master data tables
|
||||||
|
- 🔍 **Progress Tracking**: Detailed logging and error reporting
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build the Seeder CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Available Tables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-list
|
||||||
|
# or
|
||||||
|
./bin/seeder list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed a Specific Table
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-seed TABLE=province
|
||||||
|
# or
|
||||||
|
./bin/seeder seed province
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed All Tables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-seed-all
|
||||||
|
# or
|
||||||
|
./bin/seeder seed all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dry Run (Preview)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-dry-run TABLE=ethnic
|
||||||
|
# or
|
||||||
|
./bin/seeder dry-run ethnic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make seeder-validate
|
||||||
|
# or
|
||||||
|
./bin/seeder validate all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Command Line Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Seed with custom batch size and delete existing data
|
||||||
|
./bin/seeder seed province -batch-size=200 -delete-before
|
||||||
|
|
||||||
|
# Seed with custom CSV file path
|
||||||
|
./bin/seeder seed province -csv-path=/path/to/custom/provinces.csv
|
||||||
|
|
||||||
|
# Seed with custom table name
|
||||||
|
./bin/seeder seed province -table-name=provinces_backup
|
||||||
|
|
||||||
|
# Dry run with specific options
|
||||||
|
./bin/seeder dry-run ethnic -batch-size=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile Targets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Advanced seeding with options
|
||||||
|
make seeder-advanced TABLE=province BATCH_SIZE=200 DELETE_BEFORE=1
|
||||||
|
|
||||||
|
# Build seeder
|
||||||
|
make seeder-build
|
||||||
|
|
||||||
|
# Clean seeder binary
|
||||||
|
make clean-seeder
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Table Configuration
|
||||||
|
|
||||||
|
Each table has a configuration that includes:
|
||||||
|
|
||||||
|
- **TableName**: Database table name
|
||||||
|
- **CSVFile**: Path to CSV file
|
||||||
|
- **ColumnMap**: Mapping between CSV headers and struct fields
|
||||||
|
- **DeleteBefore**: Whether to delete existing data before seeding
|
||||||
|
- **BatchSize**: Number of records to insert per batch
|
||||||
|
|
||||||
|
### Default Registry
|
||||||
|
|
||||||
|
The seeder comes with pre-configured tables:
|
||||||
|
|
||||||
|
| Table | CSV File | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| province | provinces.csv | Province data |
|
||||||
|
| regency | regencies.csv | Regency/City data |
|
||||||
|
| district | districts.csv | District data |
|
||||||
|
| village | villages.csv | Village data |
|
||||||
|
| ethnic | ethnics.csv | Ethnic groups |
|
||||||
|
| language | languages.csv | Languages |
|
||||||
|
| installation | installations.csv | Hospital installations |
|
||||||
|
| unit | units.csv | Medical units |
|
||||||
|
| specialist | specialists.csv | Medical specialists |
|
||||||
|
| subspecialist | subspecialists.csv | Medical subspecialists |
|
||||||
|
|
||||||
|
### CSV File Format
|
||||||
|
|
||||||
|
CSV files should follow these conventions:
|
||||||
|
|
||||||
|
1. **Header Row**: First row must contain column names
|
||||||
|
2. **Column Names**: Use snake_case or PascalCase
|
||||||
|
3. **Data Types**: Automatic type conversion is supported
|
||||||
|
4. **Empty Values**: Empty cells are handled gracefully
|
||||||
|
|
||||||
|
Example CSV format:
|
||||||
|
```csv
|
||||||
|
Code,Name,Status
|
||||||
|
001,General Medicine,true
|
||||||
|
002,Cardiology,true
|
||||||
|
003,Neurology,true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending the Seeder
|
||||||
|
|
||||||
|
### Adding New Tables
|
||||||
|
|
||||||
|
1. **Create Entity Struct** (if not exists):
|
||||||
|
```go
|
||||||
|
type MyEntity struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Status bool `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Register in DefaultRegistry**:
|
||||||
|
```go
|
||||||
|
registry.Register("mytable", TableConfig{
|
||||||
|
TableName: "MyTable",
|
||||||
|
CSVFile: filepath.Join(basePath, "mytable.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add to GetEntityByTableName**:
|
||||||
|
```go
|
||||||
|
case "mytable":
|
||||||
|
return &MyEntity{}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Column Mapping
|
||||||
|
|
||||||
|
The seeder supports flexible column mapping:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"CSV_Column_Name": "StructFieldName",
|
||||||
|
"province_code": "ProvinceCode",
|
||||||
|
"full_name": "Name",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Type Conversion
|
||||||
|
|
||||||
|
For complex types, you can extend the `setFieldValue` function in `MasterSeeder`.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The seeder provides detailed error messages for:
|
||||||
|
|
||||||
|
- **File Not Found**: CSV file doesn't exist
|
||||||
|
- **Invalid CSV Format**: Malformed CSV files
|
||||||
|
- **Database Errors**: Connection issues, constraint violations
|
||||||
|
- **Type Conversion**: Invalid data types
|
||||||
|
- **Validation**: Missing required fields
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
The seeder provides comprehensive logging:
|
||||||
|
|
||||||
|
```
|
||||||
|
2024-01-01 12:00:00 [INFO] Starting seed for table: province
|
||||||
|
2024-01-01 12:00:00 [INFO] CSV file: internal/infrastructure/database/csv/provinces.csv
|
||||||
|
2024-01-01 12:00:00 [INFO] Batch size: 50
|
||||||
|
2024-01-01 12:00:00 [INFO] Delete before: true
|
||||||
|
2024-01-01 12:00:00 [INFO] Seeded province: 11 - ACEH
|
||||||
|
2024-01-01 12:00:00 [INFO] Seeding completed. 38 records processed from Province
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Batch Size**: Use larger batch sizes (100-500) for better performance
|
||||||
|
2. **Indexing**: Consider dropping indexes during seeding for large datasets
|
||||||
|
3. **Transactions**: Each batch is processed in a transaction
|
||||||
|
4. **Dry Run**: Always use dry-run first to validate data
|
||||||
|
5. **Delete Strategy**: Use `delete-before` for clean slate seeding
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database connection (optional, defaults to localhost)
|
||||||
|
export DATABASE_URL="host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **CSV File Not Found**
|
||||||
|
```
|
||||||
|
Error: CSV file not found: internal/infrastructure/database/csv/provinces.csv
|
||||||
|
```
|
||||||
|
Solution: Ensure CSV files are in the correct directory
|
||||||
|
|
||||||
|
2. **Database Connection Failed**
|
||||||
|
```
|
||||||
|
Error: Failed to connect to database: ...
|
||||||
|
```
|
||||||
|
Solution: Check DATABASE_URL environment variable
|
||||||
|
|
||||||
|
3. **Invalid Column Mapping**
|
||||||
|
```
|
||||||
|
Error: Failed to set field X: field not found
|
||||||
|
```
|
||||||
|
Solution: Check ColumnMap configuration
|
||||||
|
|
||||||
|
4. **Type Conversion Error**
|
||||||
|
```
|
||||||
|
Error: Failed to parse int from 'ABC': ...
|
||||||
|
```
|
||||||
|
Solution: Check CSV data types match expected format
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Use dry-run mode to preview data without inserting:
|
||||||
|
```bash
|
||||||
|
./bin/seeder dry-run province
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Add tests for new functionality
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This seeder is part of the service-general project and follows the same license terms.
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package seeders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SeederCLI adalah command-line interface untuk seeder
|
||||||
|
type SeederCLI struct {
|
||||||
|
db *gorm.DB
|
||||||
|
registry *SeederRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSeederCLI membuat CLI baru
|
||||||
|
func NewSeederCLI(db *gorm.DB) *SeederCLI {
|
||||||
|
return &SeederCLI{
|
||||||
|
db: db,
|
||||||
|
registry: DefaultRegistry(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run menjalankan CLI seeder
|
||||||
|
func (c *SeederCLI) Run(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return c.showHelp()
|
||||||
|
}
|
||||||
|
|
||||||
|
command := args[0]
|
||||||
|
switch command {
|
||||||
|
case "list":
|
||||||
|
return c.listTables()
|
||||||
|
case "seed":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("usage: seeder seed <table-name|all> [options]")
|
||||||
|
}
|
||||||
|
return c.seedTable(args[1], args[2:])
|
||||||
|
case "dry-run":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("usage: seeder dry-run <table-name|all> [options]")
|
||||||
|
}
|
||||||
|
return c.dryRunTable(args[1], args[2:])
|
||||||
|
case "validate":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("usage: seeder validate <table-name|all>")
|
||||||
|
}
|
||||||
|
return c.validateTable(args[1])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown command: %s", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showHelp menampilkan bantuan
|
||||||
|
func (c *SeederCLI) showHelp() error {
|
||||||
|
help := `
|
||||||
|
Seeder CLI - Flexible Database Seeder
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
seeder <command> [arguments]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list Show all available tables
|
||||||
|
seed <table|all> Seed data for specific table or all tables
|
||||||
|
dry-run <table|all> Preview seeding without actual insertion
|
||||||
|
validate <table|all> Validate CSV files and configuration
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
seeder list
|
||||||
|
seeder seed province
|
||||||
|
seeder seed all
|
||||||
|
seeder dry-run ethnic
|
||||||
|
seeder validate regency
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-batch-size=N Set batch size for insertion (default: from config)
|
||||||
|
-delete-before Delete existing data before seeding
|
||||||
|
-no-delete Don't delete existing data (default)
|
||||||
|
-csv-path=PATH Override CSV file path
|
||||||
|
-table-name=NAME Override table name
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- CSV files should be in internal/infrastructure/database/csv/
|
||||||
|
- First row of CSV should be header with column names
|
||||||
|
- Column names in CSV will be mapped to struct fields
|
||||||
|
`
|
||||||
|
fmt.Println(help)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listTables menampilkan daftar tabel yang tersedia
|
||||||
|
func (c *SeederCLI) listTables() error {
|
||||||
|
tables := c.registry.List()
|
||||||
|
|
||||||
|
fmt.Println("Available tables for seeding:")
|
||||||
|
fmt.Println(strings.Repeat("-", 50))
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
config, _ := c.registry.Get(table)
|
||||||
|
fmt.Printf("%-20s %s\n", table, config.CSVFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nTotal: %d tables\n", len(tables))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// seedTable melakukan seeding untuk tabel tertentu
|
||||||
|
func (c *SeederCLI) seedTable(tableName string, options []string) error {
|
||||||
|
if tableName == "all" {
|
||||||
|
return c.seedAllTables(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, exists := c.registry.Get(tableName)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
opts := c.parseOptions(options)
|
||||||
|
|
||||||
|
// Override config dengan options
|
||||||
|
if opts.CSVPath != "" {
|
||||||
|
config.CSVFile = opts.CSVPath
|
||||||
|
}
|
||||||
|
if opts.TableName != "" {
|
||||||
|
config.TableName = opts.TableName
|
||||||
|
}
|
||||||
|
if opts.BatchSize > 0 {
|
||||||
|
config.BatchSize = opts.BatchSize
|
||||||
|
}
|
||||||
|
if opts.DeleteBefore {
|
||||||
|
config.DeleteBefore = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi config
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity
|
||||||
|
entity := GetEntityByTableName(config.TableName)
|
||||||
|
if entity == nil {
|
||||||
|
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create seeder
|
||||||
|
seederConfig := SeederConfig{
|
||||||
|
CSVPath: config.CSVFile,
|
||||||
|
TableName: config.TableName,
|
||||||
|
ColumnMap: config.ColumnMap,
|
||||||
|
SkipHeader: true,
|
||||||
|
BatchSize: config.BatchSize,
|
||||||
|
DeleteBefore: config.DeleteBefore,
|
||||||
|
DryRun: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||||
|
|
||||||
|
log.Printf("Starting seed for table: %s", config.TableName)
|
||||||
|
log.Printf("CSV file: %s", config.CSVFile)
|
||||||
|
log.Printf("Batch size: %d", config.BatchSize)
|
||||||
|
log.Printf("Delete before: %v", config.DeleteBefore)
|
||||||
|
|
||||||
|
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||||
|
return fmt.Errorf("seeding failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully seeded table: %s", config.TableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dryRunTable melakukan dry-run untuk tabel tertentu
|
||||||
|
func (c *SeederCLI) dryRunTable(tableName string, options []string) error {
|
||||||
|
if tableName == "all" {
|
||||||
|
return c.dryRunAllTables(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, exists := c.registry.Get(tableName)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
opts := c.parseOptions(options)
|
||||||
|
|
||||||
|
// Override config dengan options
|
||||||
|
if opts.CSVPath != "" {
|
||||||
|
config.CSVFile = opts.CSVPath
|
||||||
|
}
|
||||||
|
if opts.TableName != "" {
|
||||||
|
config.TableName = opts.TableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi config
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity
|
||||||
|
entity := GetEntityByTableName(config.TableName)
|
||||||
|
if entity == nil {
|
||||||
|
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create seeder dengan dry-run mode
|
||||||
|
seederConfig := SeederConfig{
|
||||||
|
CSVPath: config.CSVFile,
|
||||||
|
TableName: config.TableName,
|
||||||
|
ColumnMap: config.ColumnMap,
|
||||||
|
SkipHeader: true,
|
||||||
|
BatchSize: 10, // Smaller batch for dry-run
|
||||||
|
DeleteBefore: false,
|
||||||
|
DryRun: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
seeder := NewMasterSeeder(c.db, seederConfig)
|
||||||
|
|
||||||
|
log.Printf("Starting dry-run for table: %s", config.TableName)
|
||||||
|
log.Printf("CSV file: %s", config.CSVFile)
|
||||||
|
|
||||||
|
if err := seeder.SeedFromCSV(entity); err != nil {
|
||||||
|
return fmt.Errorf("dry-run failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Dry-run completed for table: %s", config.TableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTable memvalidasi konfigurasi dan CSV file
|
||||||
|
func (c *SeederCLI) validateTable(tableName string) error {
|
||||||
|
if tableName == "all" {
|
||||||
|
return c.validateAllTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
config, exists := c.registry.Get(tableName)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("table '%s' not found in registry", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi config
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity
|
||||||
|
entity := GetEntityByTableName(config.TableName)
|
||||||
|
if entity == nil {
|
||||||
|
return fmt.Errorf("entity not found for table '%s'", config.TableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baca CSV file dan validasi
|
||||||
|
file, err := os.Open(config.CSVFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := csv.NewReader(file)
|
||||||
|
headers, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung jumlah baris
|
||||||
|
rowCount := 0
|
||||||
|
for {
|
||||||
|
_, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rowCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Validation passed for table: %s\n", tableName)
|
||||||
|
fmt.Printf("CSV file: %s\n", config.CSVFile)
|
||||||
|
fmt.Printf("Headers: %v\n", headers)
|
||||||
|
fmt.Printf("Expected rows: %d\n", rowCount)
|
||||||
|
fmt.Printf("Column mapping: %v\n", config.ColumnMap)
|
||||||
|
fmt.Printf("Entity type: %T\n", entity)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// seedAllTables melakukan seeding untuk semua tabel
|
||||||
|
func (c *SeederCLI) seedAllTables(options []string) error {
|
||||||
|
tables := c.registry.List()
|
||||||
|
|
||||||
|
log.Printf("Starting seed for all tables (%d tables)", len(tables))
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if err := c.seedTable(table, options); err != nil {
|
||||||
|
log.Printf("Error seeding table %s: %v", table, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Completed seeding all tables")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dryRunAllTables melakukan dry-run untuk semua tabel
|
||||||
|
func (c *SeederCLI) dryRunAllTables(options []string) error {
|
||||||
|
tables := c.registry.List()
|
||||||
|
|
||||||
|
log.Printf("Starting dry-run for all tables (%d tables)", len(tables))
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if err := c.dryRunTable(table, options); err != nil {
|
||||||
|
log.Printf("Error in dry-run for table %s: %v", table, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Completed dry-run for all tables")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAllTables memvalidasi semua tabel
|
||||||
|
func (c *SeederCLI) validateAllTables() error {
|
||||||
|
tables := c.registry.List()
|
||||||
|
|
||||||
|
fmt.Printf("Validating all tables (%d tables):\n", len(tables))
|
||||||
|
fmt.Println(strings.Repeat("=", 60))
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
fmt.Printf("\n[%s]\n", table)
|
||||||
|
if err := c.validateTable(table); err != nil {
|
||||||
|
fmt.Printf("❌ Validation failed: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Validation passed\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeederOptions berisi parsed options
|
||||||
|
type SeederOptions struct {
|
||||||
|
CSVPath string
|
||||||
|
TableName string
|
||||||
|
BatchSize int
|
||||||
|
DeleteBefore bool
|
||||||
|
DryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptions mem-parsing command line options
|
||||||
|
func (c *SeederCLI) parseOptions(options []string) SeederOptions {
|
||||||
|
var opts SeederOptions
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(option, "-batch-size="):
|
||||||
|
fmt.Sscanf(option, "-batch-size=%d", &opts.BatchSize)
|
||||||
|
case option == "-delete-before":
|
||||||
|
opts.DeleteBefore = true
|
||||||
|
case strings.HasPrefix(option, "-csv-path="):
|
||||||
|
opts.CSVPath = strings.TrimPrefix(option, "-csv-path=")
|
||||||
|
case strings.HasPrefix(option, "-table-name="):
|
||||||
|
opts.TableName = strings.TrimPrefix(option, "-table-name=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// MainCLI adalah entry point utama untuk CLI
|
||||||
|
func MainCLI() {
|
||||||
|
// Setup database connection
|
||||||
|
db, err := setupDatabase()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := NewSeederCLI(db)
|
||||||
|
|
||||||
|
if err := cli.Run(os.Args[1:]); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupDatabase setup koneksi database
|
||||||
|
func setupDatabase() (*gorm.DB, error) {
|
||||||
|
// Bisa diambil dari config atau environment variable
|
||||||
|
dsn := os.Getenv("DATABASE_URL")
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = "host=localhost user=postgres password=postgres dbname=health port=5432 sslmode=disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
package seeders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TableConfig berisi konfigurasi untuk setiap tabel
|
||||||
|
type TableConfig struct {
|
||||||
|
TableName string
|
||||||
|
CSVFile string
|
||||||
|
Entity interface{}
|
||||||
|
ColumnMap map[string]string
|
||||||
|
DeleteBefore bool
|
||||||
|
BatchSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeederRegistry berisi registry untuk semua tabel yang bisa di-seed
|
||||||
|
type SeederRegistry struct {
|
||||||
|
tables map[string]TableConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSeederRegistry membuat registry baru
|
||||||
|
func NewSeederRegistry() *SeederRegistry {
|
||||||
|
return &SeederRegistry{
|
||||||
|
tables: make(map[string]TableConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register mendaftarkan tabel ke registry
|
||||||
|
func (r *SeederRegistry) Register(name string, config TableConfig) {
|
||||||
|
r.tables[name] = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mendapatkan konfigurasi tabel berdasarkan nama
|
||||||
|
func (r *SeederRegistry) Get(name string) (TableConfig, bool) {
|
||||||
|
config, exists := r.tables[name]
|
||||||
|
return config, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// List mengembalikan daftar nama tabel yang terdaftar
|
||||||
|
func (r *SeederRegistry) List() []string {
|
||||||
|
names := make([]string, 0, len(r.tables))
|
||||||
|
for name := range r.tables {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRegistry berisi konfigurasi default untuk semua tabel
|
||||||
|
func DefaultRegistry() *SeederRegistry {
|
||||||
|
registry := NewSeederRegistry()
|
||||||
|
|
||||||
|
basePath := "internal/infrastructure/database/csv"
|
||||||
|
|
||||||
|
// Register Province
|
||||||
|
registry.Register("province", TableConfig{
|
||||||
|
TableName: "Province",
|
||||||
|
CSVFile: filepath.Join(basePath, "provinces.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Regency
|
||||||
|
registry.Register("regency", TableConfig{
|
||||||
|
TableName: "Regency",
|
||||||
|
CSVFile: filepath.Join(basePath, "regencies.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Province_Code": "ProvinceCode",
|
||||||
|
"Name": "Name",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register District
|
||||||
|
registry.Register("district", TableConfig{
|
||||||
|
TableName: "District",
|
||||||
|
CSVFile: filepath.Join(basePath, "districts.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Regency_Code": "RegencyCode",
|
||||||
|
"Name": "Name",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 200,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Village
|
||||||
|
registry.Register("village", TableConfig{
|
||||||
|
TableName: "Village",
|
||||||
|
CSVFile: filepath.Join(basePath, "villages.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"District_Code": "DistrictCode",
|
||||||
|
"Name": "Name",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Ethnic
|
||||||
|
registry.Register("ethnic", TableConfig{
|
||||||
|
TableName: "Ethnic",
|
||||||
|
CSVFile: filepath.Join(basePath, "ethnics.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Language
|
||||||
|
registry.Register("language", TableConfig{
|
||||||
|
TableName: "Language",
|
||||||
|
CSVFile: filepath.Join(basePath, "languages.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Installation
|
||||||
|
registry.Register("installation", TableConfig{
|
||||||
|
TableName: "Installation",
|
||||||
|
CSVFile: filepath.Join(basePath, "installations.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Unit
|
||||||
|
registry.Register("unit", TableConfig{
|
||||||
|
TableName: "Unit",
|
||||||
|
CSVFile: filepath.Join(basePath, "units.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register Specialist
|
||||||
|
registry.Register("specialist", TableConfig{
|
||||||
|
TableName: "Specialist",
|
||||||
|
CSVFile: filepath.Join(basePath, "specialists.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register SubSpecialist
|
||||||
|
registry.Register("subspecialist", TableConfig{
|
||||||
|
TableName: "SubSpecialist",
|
||||||
|
CSVFile: filepath.Join(basePath, "subspecialists.csv"),
|
||||||
|
ColumnMap: map[string]string{
|
||||||
|
"Code": "Code",
|
||||||
|
"Specialist_Code": "SpecialistCode",
|
||||||
|
"Name": "Name",
|
||||||
|
"Status": "Status",
|
||||||
|
},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register RolPages (Menu Structure)
|
||||||
|
registry.Register("rol_pages", TableConfig{
|
||||||
|
TableName: "rol_pages",
|
||||||
|
Entity: &RolPages{},
|
||||||
|
DeleteBefore: true,
|
||||||
|
BatchSize: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig memvalidasi konfigurasi
|
||||||
|
func (c *TableConfig) Validate() error {
|
||||||
|
if c.TableName == "" {
|
||||||
|
return fmt.Errorf("table name is required")
|
||||||
|
}
|
||||||
|
if c.CSVFile == "" {
|
||||||
|
return fmt.Errorf("CSV file path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah file CSV ada
|
||||||
|
if _, err := os.Stat(c.CSVFile); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("CSV file not found: %s", c.CSVFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BatchSize <= 0 {
|
||||||
|
c.BatchSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEntityByTableName mendapatkan entity berdasarkan nama tabel
|
||||||
|
func GetEntityByTableName(tableName string) interface{} {
|
||||||
|
switch strings.ToLower(tableName) {
|
||||||
|
case "province":
|
||||||
|
return &Province{}
|
||||||
|
case "regency":
|
||||||
|
return &Regency{}
|
||||||
|
case "district":
|
||||||
|
return &District{}
|
||||||
|
case "village":
|
||||||
|
return &Village{}
|
||||||
|
// case "ethnic":
|
||||||
|
// return &Ethnic{}
|
||||||
|
case "language":
|
||||||
|
return &Language{}
|
||||||
|
case "installation":
|
||||||
|
return &Installation{}
|
||||||
|
case "unit":
|
||||||
|
return &Unit{}
|
||||||
|
case "specialist":
|
||||||
|
return &Specialist{}
|
||||||
|
case "subspecialist":
|
||||||
|
return &SubSpecialist{}
|
||||||
|
case "rol_pages":
|
||||||
|
return &RolPages{}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity definitions
|
||||||
|
|
||||||
|
type Province struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Regency struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
ProvinceCode string `gorm:"index;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type District struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
RegencyCode string `gorm:"index;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Village struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
DistrictCode string `gorm:"index;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Language struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||||
|
Name interface{} `gorm:"not null"`
|
||||||
|
Status interface{} `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Installation struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||||
|
Name interface{} `gorm:"not null"`
|
||||||
|
Status interface{} `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unit struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||||
|
Name interface{} `gorm:"not null"`
|
||||||
|
Status interface{} `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Specialist struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||||
|
Name interface{} `gorm:"not null"`
|
||||||
|
Status interface{} `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubSpecialist struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code interface{} `gorm:"uniqueIndex;not null"`
|
||||||
|
SpecialistCode interface{} `gorm:"index;not null"`
|
||||||
|
Name interface{} `gorm:"not null"`
|
||||||
|
Status interface{} `gorm:"default:true"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolPages struct untuk menu hierarchy
|
||||||
|
type RolPages struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `gorm:"not null;size:20"`
|
||||||
|
Icon string `gorm:"size:100"`
|
||||||
|
URL string `gorm:"not null"`
|
||||||
|
Level int16 `gorm:"not null;default:0"`
|
||||||
|
Sort int16 `gorm:"not null;default:0"`
|
||||||
|
Parent *int64 `gorm:"index"`
|
||||||
|
Active bool `gorm:"not null;default:true"`
|
||||||
|
CreatedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP"`
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
package seeders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SeederConfig berisi konfigurasi untuk seeding
|
||||||
|
type SeederConfig struct {
|
||||||
|
CSVPath string
|
||||||
|
TableName string
|
||||||
|
ColumnMap map[string]string // mapping CSV header ke field struct
|
||||||
|
SkipHeader bool
|
||||||
|
BatchSize int
|
||||||
|
DryRun bool
|
||||||
|
DeleteBefore bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasterSeeder adalah struct utama untuk seeding fleksibel
|
||||||
|
type MasterSeeder struct {
|
||||||
|
db *gorm.DB
|
||||||
|
config SeederConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMasterSeeder membuat instance seeder baru
|
||||||
|
func NewMasterSeeder(db *gorm.DB, config SeederConfig) *MasterSeeder {
|
||||||
|
if config.BatchSize == 0 {
|
||||||
|
config.BatchSize = 100
|
||||||
|
}
|
||||||
|
return &MasterSeeder{
|
||||||
|
db: db,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedFromCSV melakukan seeding dari file CSV
|
||||||
|
func (s *MasterSeeder) SeedFromCSV(model interface{}) error {
|
||||||
|
if s.config.CSVPath == "" {
|
||||||
|
return fmt.Errorf("CSV path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(s.config.CSVPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open CSV file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := csv.NewReader(file)
|
||||||
|
reader.Comma = ','
|
||||||
|
|
||||||
|
// Baca header
|
||||||
|
headers, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read CSV header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("CSV Headers: %v", headers)
|
||||||
|
|
||||||
|
// Hapus data lama jika diminta
|
||||||
|
if s.config.DeleteBefore {
|
||||||
|
if err := s.db.Where("1=1").Delete(model).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to delete existing data: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("Deleted existing data from %s", s.config.TableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
batch := []interface{}{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
record, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "EOF" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("Error reading row %d: %v", count+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(record) < len(headers) {
|
||||||
|
log.Printf("Skipping incomplete row %d", count+1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat instance model baru
|
||||||
|
instance := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||||
|
|
||||||
|
// Mapping data dari CSV ke struct
|
||||||
|
if err := s.mapCSVToStruct(headers, record, instance); err != nil {
|
||||||
|
log.Printf("Error mapping row %d: %v", count+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set audit fields
|
||||||
|
s.setAuditFields(instance)
|
||||||
|
|
||||||
|
if s.config.DryRun {
|
||||||
|
log.Printf("Dry run - Would insert: %+v", instance)
|
||||||
|
} else {
|
||||||
|
batch = append(batch, instance)
|
||||||
|
|
||||||
|
// Insert batch jika sudah penuh
|
||||||
|
if len(batch) >= s.config.BatchSize {
|
||||||
|
if err := s.insertBatch(batch); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert batch: %w", err)
|
||||||
|
}
|
||||||
|
count += len(batch)
|
||||||
|
batch = []interface{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert sisa batch
|
||||||
|
if len(batch) > 0 && !s.config.DryRun {
|
||||||
|
if err := s.insertBatch(batch); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert final batch: %w", err)
|
||||||
|
}
|
||||||
|
count += len(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Seeding completed. %d records processed from %s", count, s.config.TableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapCSVToStruct mapping data CSV ke struct berdasarkan column map atau reflection
|
||||||
|
func (s *MasterSeeder) mapCSVToStruct(headers []string, record []string, instance interface{}) error {
|
||||||
|
v := reflect.ValueOf(instance).Elem()
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
for i, header := range headers {
|
||||||
|
if i >= len(record) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.TrimSpace(record[i])
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cari field berdasarkan column map atau nama header
|
||||||
|
fieldName := s.getFieldName(header)
|
||||||
|
|
||||||
|
field, found := t.FieldByName(fieldName)
|
||||||
|
if !found {
|
||||||
|
// Coba dengan nama field yang berbeda case
|
||||||
|
for j := 0; j < t.NumField(); j++ {
|
||||||
|
f := t.Field(j)
|
||||||
|
if strings.EqualFold(f.Name, fieldName) {
|
||||||
|
field = f
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
if err := s.setFieldValue(v.FieldByName(field.Name), value); err != nil {
|
||||||
|
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFieldName mendapatkan nama field dari header CSV
|
||||||
|
func (s *MasterSeeder) getFieldName(header string) string {
|
||||||
|
// Cek di column map dulu
|
||||||
|
if fieldName, exists := s.config.ColumnMap[header]; exists {
|
||||||
|
return fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konversi header ke PascalCase
|
||||||
|
words := strings.Split(header, "_")
|
||||||
|
for i, word := range words {
|
||||||
|
words[i] = strings.Title(strings.ToLower(word))
|
||||||
|
}
|
||||||
|
return strings.Join(words, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setFieldValue mengisi value ke field struct dengan type conversion
|
||||||
|
func (s *MasterSeeder) setFieldValue(field reflect.Value, value string) error {
|
||||||
|
if !field.CanSet() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
field.SetString(value)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
// Handle bigserial (int64)
|
||||||
|
if value == "" {
|
||||||
|
field.SetInt(0)
|
||||||
|
} else {
|
||||||
|
intVal, err := s.parseInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetInt(intVal)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
boolVal := strings.ToLower(value) == "true" || value == "1"
|
||||||
|
field.SetBool(boolVal)
|
||||||
|
case reflect.Interface:
|
||||||
|
// Untuk field interface{} (seperti Code, Name, Status pada Ethnic)
|
||||||
|
field.Set(reflect.ValueOf(value))
|
||||||
|
default:
|
||||||
|
// Handle pointer types
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
if value == "" {
|
||||||
|
field.Set(reflect.Zero(field.Type()))
|
||||||
|
} else {
|
||||||
|
// Create new instance of the pointed type
|
||||||
|
newValue := reflect.New(field.Type().Elem())
|
||||||
|
if err := s.setFieldValue(newValue.Elem(), value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.Set(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt parsing string ke int64
|
||||||
|
func (s *MasterSeeder) parseInt(value string) (int64, error) {
|
||||||
|
var intVal int64
|
||||||
|
_, err := fmt.Sscanf(value, "%d", &intVal)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to parse int from '%s': %w", value, err)
|
||||||
|
}
|
||||||
|
return intVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setAuditFields mengisi field audit (CreatedAt, UpdatedAt, DeletedAt)
|
||||||
|
func (s *MasterSeeder) setAuditFields(instance interface{}) {
|
||||||
|
v := reflect.ValueOf(instance).Elem()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Set CreatedAt jika field ada
|
||||||
|
if field := v.FieldByName("CreatedAt"); field.IsValid() && field.CanSet() {
|
||||||
|
if field.Kind() == reflect.Struct {
|
||||||
|
// Handle time.Time
|
||||||
|
field.Set(reflect.ValueOf(now))
|
||||||
|
} else if field.Kind() == reflect.Interface {
|
||||||
|
field.Set(reflect.ValueOf(now))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set UpdatedAt jika field ada (pointer)
|
||||||
|
if field := v.FieldByName("UpdatedAt"); field.IsValid() && field.CanSet() {
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
field.Set(reflect.ValueOf(&now))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set DeletedAt ke nil jika field ada
|
||||||
|
if field := v.FieldByName("DeletedAt"); field.IsValid() && field.CanSet() {
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
field.Set(reflect.Zero(field.Type()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertBatch insert batch data ke database
|
||||||
|
func (s *MasterSeeder) insertBatch(batch []interface{}) error {
|
||||||
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, item := range batch {
|
||||||
|
// Gunakan FirstOrCreate untuk menghindari duplicate
|
||||||
|
v := reflect.ValueOf(item).Elem()
|
||||||
|
|
||||||
|
// Buat kondisi where berdasarkan field utama (Code atau ID)
|
||||||
|
var condition interface{}
|
||||||
|
if codeField := v.FieldByName("Code"); codeField.IsValid() {
|
||||||
|
condition = map[string]interface{}{"Code": codeField.Interface()}
|
||||||
|
} else if idField := v.FieldByName("Id"); idField.IsValid() {
|
||||||
|
condition = map[string]interface{}{"Id": idField.Interface()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition != nil {
|
||||||
|
result := tx.Where(condition).FirstOrCreate(item)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Jika tidak ada kondisi, insert langsung
|
||||||
|
if err := tx.Create(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions untuk seeding spesifik
|
||||||
|
|
||||||
|
// SeedProvinces seed data provinsi dari CSV
|
||||||
|
func SeedProvinces(db *gorm.DB, csvPath string) error {
|
||||||
|
config := SeederConfig{
|
||||||
|
CSVPath: csvPath,
|
||||||
|
TableName: "Province",
|
||||||
|
ColumnMap: map[string]string{"Code": "Code", "Name": "Name"},
|
||||||
|
SkipHeader: true,
|
||||||
|
BatchSize: 50,
|
||||||
|
DeleteBefore: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
seeder := NewMasterSeeder(db, config)
|
||||||
|
|
||||||
|
// Definisikan struct untuk Province
|
||||||
|
type Province struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return seeder.SeedFromCSV(&Province{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedRegencies seed data kabupaten dari CSV
|
||||||
|
func SeedRegencies(db *gorm.DB, csvPath string) error {
|
||||||
|
config := SeederConfig{
|
||||||
|
CSVPath: csvPath,
|
||||||
|
TableName: "Regency",
|
||||||
|
ColumnMap: map[string]string{"Code": "Code", "Province_Code": "ProvinceCode", "Name": "Name"},
|
||||||
|
SkipHeader: true,
|
||||||
|
BatchSize: 100,
|
||||||
|
DeleteBefore: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
seeder := NewMasterSeeder(db, config)
|
||||||
|
|
||||||
|
type Regency struct {
|
||||||
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null"`
|
||||||
|
ProvinceCode string `gorm:"index;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return seeder.SeedFromCSV(&Regency{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedEthnics seed data suku bangsa dari CSV
|
||||||
|
// func SeedEthnics(db *gorm.DB, csvPath string) error {
|
||||||
|
// config := SeederConfig{
|
||||||
|
// CSVPath: csvPath,
|
||||||
|
// TableName: "Ethnic",
|
||||||
|
// ColumnMap: map[string]string{"Code": "Code", "Name": "Name", "Status": "Status"},
|
||||||
|
// SkipHeader: true,
|
||||||
|
// BatchSize: 50,
|
||||||
|
// DeleteBefore: true,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// seeder := NewMasterSeeder(db, config)
|
||||||
|
|
||||||
|
// // Gunakan struct Ethnic yang sudah ada
|
||||||
|
// return seeder.SeedFromCSV(&Ethnic{})
|
||||||
|
// }
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS rol_pages_id_seq;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rol_pages (
|
||||||
|
id int4 NOT NULL DEFAULT nextval('rol_pages_id_seq'),
|
||||||
|
"name" varchar(20) NOT NULL,
|
||||||
|
icon varchar(100) NULL,
|
||||||
|
url text NOT NULL,
|
||||||
|
"level" int2 NOT NULL DEFAULT 0,
|
||||||
|
sort int2 NOT NULL DEFAULT 0,
|
||||||
|
parent int4 NULL,
|
||||||
|
active bool NOT NULL DEFAULT true,
|
||||||
|
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at timestamp NULL,
|
||||||
|
deleted_at timestamp NULL,
|
||||||
|
CONSTRAINT rol_pages_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT rol_pages_parent_fkey FOREIGN KEY (parent) REFERENCES rol_pages(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index untuk performa query
|
||||||
|
CREATE INDEX idx_rol_pages_parent ON rol_pages(parent) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_rol_pages_level ON rol_pages(level) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_rol_pages_active ON rol_pages(active) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_rol_pages_sort ON rol_pages(sort) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- Trigger untuk update updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$ language 'plpgsql';
|
||||||
|
|
||||||
|
CREATE TRIGGER update_rol_pages_updated_at BEFORE UPDATE ON rol_pages
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TRIGGER IF EXISTS update_rol_pages_updated_at ON rol_pages;
|
||||||
|
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||||
|
DROP INDEX IF EXISTS idx_rol_pages_parent;
|
||||||
|
DROP INDEX IF EXISTS idx_rol_pages_level;
|
||||||
|
DROP INDEX IF EXISTS idx_rol_pages_active;
|
||||||
|
DROP INDEX IF EXISTS idx_rol_pages_sort;
|
||||||
|
DROP TABLE IF EXISTS rol_pages;
|
||||||
|
DROP SEQUENCE IF EXISTS rol_pages_id_seq;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE rol_component (
|
||||||
|
id int4 NOT NULL,
|
||||||
|
"name" varchar(100) NULL,
|
||||||
|
description text NULL,
|
||||||
|
directory text NOT NULL,
|
||||||
|
active bool NOT NULL,
|
||||||
|
fk_rol_pages_id int4 NOT NULL,
|
||||||
|
sort int2 NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
SELECT 'down SQL query';
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE rol_permission (
|
||||||
|
id int4 NOT NULL,
|
||||||
|
"create" bool NULL,
|
||||||
|
"read" bool NULL,
|
||||||
|
"update" bool NULL,
|
||||||
|
"disable" bool NULL,
|
||||||
|
"delete" bool NULL,
|
||||||
|
active bool NULL,
|
||||||
|
fk_rol_pages_id int4 NULL,
|
||||||
|
role_keycloak _text NULL,
|
||||||
|
group_keycloak _text NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
SELECT 'down SQL query';
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE role_access.rol_master (
|
||||||
|
id int4 NOT NULL,
|
||||||
|
"name" varchar(100) NULL,
|
||||||
|
active bool NULL,
|
||||||
|
created_at timestamp NULL,
|
||||||
|
updated_at timestamp NULL,
|
||||||
|
CONSTRAINT rol_master_role_pk PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
SELECT 'down SQL query';
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/health.go
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/internal/infrastructure/cache"
|
||||||
|
"service/internal/infrastructure/config"
|
||||||
|
"service/internal/infrastructure/database"
|
||||||
|
"service/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HealthMonitor handles health monitoring and metrics collection
|
||||||
|
type HealthMonitor struct {
|
||||||
|
metrics *Metrics
|
||||||
|
dbService database.Service
|
||||||
|
cacheManager *cache.Manager
|
||||||
|
config *config.Config
|
||||||
|
startTime time.Time
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHealthMonitor creates a new health monitor
|
||||||
|
func NewHealthMonitor(metrics *Metrics, dbService database.Service, cacheManager *cache.Manager, config *config.Config) *HealthMonitor {
|
||||||
|
log := logger.Default().WithFields(
|
||||||
|
logger.String("component", "health_monitor"),
|
||||||
|
logger.String("service", "goprint"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &HealthMonitor{
|
||||||
|
metrics: metrics,
|
||||||
|
dbService: dbService,
|
||||||
|
cacheManager: cacheManager,
|
||||||
|
config: config,
|
||||||
|
startTime: time.Now(),
|
||||||
|
logger: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the health monitoring routine
|
||||||
|
func (h *HealthMonitor) Start(ctx context.Context) {
|
||||||
|
log := h.logger.WithFields(logger.String("action", "start_health_monitoring"))
|
||||||
|
log.Info("Starting health monitoring service")
|
||||||
|
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// Initial health check
|
||||||
|
h.performHealthCheck()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
h.performHealthCheck()
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Info("Health monitoring service stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// performHealthCheck performs comprehensive health check
|
||||||
|
func (h *HealthMonitor) performHealthCheck() {
|
||||||
|
log := h.logger.WithFields(logger.String("action", "health_check"))
|
||||||
|
|
||||||
|
// Check all databases
|
||||||
|
dbHealthy := h.checkAllDatabases()
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
cacheHealthy := h.checkCache()
|
||||||
|
|
||||||
|
// Check external services
|
||||||
|
externalHealthy := h.checkExternalServices()
|
||||||
|
|
||||||
|
// Update service health
|
||||||
|
overallHealthy := dbHealthy && cacheHealthy
|
||||||
|
h.metrics.UpdateServiceHealth(overallHealthy)
|
||||||
|
|
||||||
|
// Update resource usage
|
||||||
|
h.updateResourceUsage()
|
||||||
|
|
||||||
|
// Update uptime
|
||||||
|
uptime := time.Since(h.startTime).Seconds()
|
||||||
|
h.metrics.IncrementUptime(uptime)
|
||||||
|
|
||||||
|
log.Info("Health check completed",
|
||||||
|
logger.Bool("database_healthy", dbHealthy),
|
||||||
|
logger.Bool("cache_healthy", cacheHealthy),
|
||||||
|
logger.Bool("external_healthy", externalHealthy),
|
||||||
|
logger.Bool("overall_healthy", overallHealthy),
|
||||||
|
logger.Float64("uptime_seconds", uptime),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAllDatabases checks all configured databases
|
||||||
|
func (h *HealthMonitor) checkAllDatabases() bool {
|
||||||
|
log := h.logger.WithFields(logger.String("action", "check_databases"))
|
||||||
|
|
||||||
|
dbList := h.dbService.ListDBs()
|
||||||
|
if len(dbList) == 0 {
|
||||||
|
log.Warn("No databases configured")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
allHealthy := true
|
||||||
|
for _, dbName := range dbList {
|
||||||
|
if !h.checkDatabase(dbName) {
|
||||||
|
allHealthy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allHealthy
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDatabase checks specific database connectivity
|
||||||
|
func (h *HealthMonitor) checkDatabase(dbName string) bool {
|
||||||
|
log := h.logger.WithFields(
|
||||||
|
logger.String("action", "check_database"),
|
||||||
|
logger.String("database", dbName),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get database type
|
||||||
|
dbType, err := h.dbService.GetDBType(dbName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get database type", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check based on database type
|
||||||
|
switch dbType {
|
||||||
|
case database.Postgres, database.MySQL, database.SQLServer, database.SQLite:
|
||||||
|
return h.checkSQLDatabase(dbName, log)
|
||||||
|
case database.MongoDB:
|
||||||
|
return h.checkMongoDatabase(dbName, log)
|
||||||
|
default:
|
||||||
|
log.Error("Unknown database type", logger.String("type", string(dbType)))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSQLDatabase checks SQL database connectivity
|
||||||
|
func (h *HealthMonitor) checkSQLDatabase(dbName string, log logger.Logger) bool {
|
||||||
|
// Get GORM DB
|
||||||
|
gormDB, err := h.dbService.GetGormDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get GORM database", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SQL DB from GORM
|
||||||
|
sqlDB, err := gormDB.DB()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get SQL database from GORM", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test connection with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = sqlDB.PingContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Database ping failed", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(0)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection stats
|
||||||
|
stats := sqlDB.Stats()
|
||||||
|
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(float64(stats.OpenConnections))
|
||||||
|
|
||||||
|
log.Info("Database connection healthy",
|
||||||
|
logger.Int("open_connections", stats.OpenConnections),
|
||||||
|
logger.Int("idle_connections", stats.Idle),
|
||||||
|
logger.Int("in_use_connections", stats.InUse),
|
||||||
|
)
|
||||||
|
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkMongoDatabase checks MongoDB connectivity
|
||||||
|
func (h *HealthMonitor) checkMongoDatabase(dbName string, log logger.Logger) bool {
|
||||||
|
client, err := h.dbService.GetMongoClient(dbName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get MongoDB client", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test connection with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = client.Ping(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("MongoDB ping failed", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("MongoDB connection healthy")
|
||||||
|
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCache checks cache connectivity
|
||||||
|
func (h *HealthMonitor) checkCache() bool {
|
||||||
|
log := h.logger.WithFields(logger.String("action", "check_cache"))
|
||||||
|
|
||||||
|
if h.cacheManager == nil {
|
||||||
|
log.Warn("Cache manager not available")
|
||||||
|
return true // Cache is optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cache health directly using manager
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := h.cacheManager.Health(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Cache health check failed", logger.ErrorField(err))
|
||||||
|
h.metrics.UpdateExternalServiceStatus("redis", false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Cache connection healthy")
|
||||||
|
h.metrics.UpdateExternalServiceStatus("redis", true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkExternalServices checks external service connectivity
|
||||||
|
func (h *HealthMonitor) checkExternalServices() bool {
|
||||||
|
allHealthy := true
|
||||||
|
|
||||||
|
// Check BPJS service if configured
|
||||||
|
if h.config != nil && h.config.Bpjs.BaseURL != "" {
|
||||||
|
bpjsHealthy := h.checkBPJSService()
|
||||||
|
if !bpjsHealthy {
|
||||||
|
allHealthy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check SatuSehat service if configured
|
||||||
|
if h.config != nil && h.config.SatuSehat.BaseURL != "" {
|
||||||
|
satuSehatHealthy := h.checkSatuSehatService()
|
||||||
|
if !satuSehatHealthy {
|
||||||
|
allHealthy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allHealthy
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkBPJSService checks BPJS service connectivity
|
||||||
|
func (h *HealthMonitor) checkBPJSService() bool {
|
||||||
|
log := h.logger.WithFields(
|
||||||
|
logger.String("action", "check_bpjs"),
|
||||||
|
logger.String("service", "bpjs"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implement BPJS health check logic here
|
||||||
|
// For now, return true as placeholder
|
||||||
|
log.Info("BPJS service health check (placeholder)")
|
||||||
|
h.metrics.UpdateExternalServiceStatus("bpjs", true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSatuSehatService checks SatuSehat service connectivity
|
||||||
|
func (h *HealthMonitor) checkSatuSehatService() bool {
|
||||||
|
log := h.logger.WithFields(
|
||||||
|
logger.String("action", "check_satu_sehat"),
|
||||||
|
logger.String("service", "satu_sehat"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implement SatuSehat health check logic here
|
||||||
|
// For now, return true as placeholder
|
||||||
|
log.Info("SatuSehat service health check (placeholder)")
|
||||||
|
h.metrics.UpdateExternalServiceStatus("satu_sehat", true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateResourceUsage updates resource usage metrics
|
||||||
|
func (h *HealthMonitor) updateResourceUsage() {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
// Memory usage in MB
|
||||||
|
memoryMB := float64(m.Alloc) / 1024 / 1024
|
||||||
|
|
||||||
|
// Number of goroutines
|
||||||
|
goroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
// CPU usage would require more complex implementation
|
||||||
|
// For now, we'll set it to 0 as placeholder
|
||||||
|
cpuUsage := 0.0
|
||||||
|
|
||||||
|
h.metrics.UpdateResourceUsage(memoryMB, cpuUsage, goroutines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHealthEndpoint registers health check endpoint with metrics
|
||||||
|
func (h *HealthMonitor) RegisterHealthEndpoint(router *gin.Engine) {
|
||||||
|
router.GET("/metrics/health", func(c *gin.Context) {
|
||||||
|
health := gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": time.Now().UTC(),
|
||||||
|
"uptime": time.Since(h.startTime).String(),
|
||||||
|
"version": "1.0.0",
|
||||||
|
"service": "service-general",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
dbHealthy := h.checkAllDatabases()
|
||||||
|
health["database"] = gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"connected": dbHealthy,
|
||||||
|
"databases": h.getDatabaseStatuses(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
cacheHealthy := h.checkCache()
|
||||||
|
health["cache"] = gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"connected": cacheHealthy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check external services
|
||||||
|
externalHealthy := h.checkExternalServices()
|
||||||
|
health["external_services"] = gin.H{
|
||||||
|
"status": "healthy",
|
||||||
|
"connected": externalHealthy,
|
||||||
|
"services": h.getExternalServiceStatuses(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine overall status
|
||||||
|
overallHealthy := dbHealthy && cacheHealthy && externalHealthy
|
||||||
|
if !overallHealthy {
|
||||||
|
health["status"] = "unhealthy"
|
||||||
|
c.JSON(503, health)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, health)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDatabaseStatuses returns detailed database statuses
|
||||||
|
func (h *HealthMonitor) getDatabaseStatuses() map[string]interface{} {
|
||||||
|
statuses := make(map[string]interface{})
|
||||||
|
|
||||||
|
dbList := h.dbService.ListDBs()
|
||||||
|
for _, dbName := range dbList {
|
||||||
|
dbType, _ := h.dbService.GetDBType(dbName)
|
||||||
|
healthy := h.checkDatabase(dbName)
|
||||||
|
|
||||||
|
statuses[dbName] = gin.H{
|
||||||
|
"type": string(dbType),
|
||||||
|
"connected": healthy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExternalServiceStatuses returns external service statuses
|
||||||
|
func (h *HealthMonitor) getExternalServiceStatuses() map[string]interface{} {
|
||||||
|
statuses := make(map[string]interface{})
|
||||||
|
|
||||||
|
if h.config != nil {
|
||||||
|
if h.config.Bpjs.BaseURL != "" {
|
||||||
|
statuses["bpjs"] = gin.H{
|
||||||
|
"connected": true, // Placeholder
|
||||||
|
"url": h.config.Bpjs.BaseURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.config.SatuSehat.BaseURL != "" {
|
||||||
|
statuses["satu_sehat"] = gin.H{
|
||||||
|
"connected": true, // Placeholder
|
||||||
|
"url": h.config.SatuSehat.BaseURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/metrics.go
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metrics defines all Prometheus metrics for the GoPrint service
|
||||||
|
type Metrics struct {
|
||||||
|
// HTTP metrics
|
||||||
|
HttpRequestsTotal *prometheus.CounterVec
|
||||||
|
HttpRequestDuration *prometheus.HistogramVec
|
||||||
|
HttpResponseSize *prometheus.HistogramVec
|
||||||
|
HttpRequestsInFlight prometheus.Gauge
|
||||||
|
|
||||||
|
// Database metrics
|
||||||
|
DbConnections prometheus.Gauge
|
||||||
|
DbQueryDuration *prometheus.HistogramVec
|
||||||
|
DbQueryErrors *prometheus.CounterVec
|
||||||
|
DbActiveConnections *prometheus.GaugeVec
|
||||||
|
|
||||||
|
// Cache metrics
|
||||||
|
CacheHits prometheus.Counter
|
||||||
|
CacheMisses prometheus.Counter
|
||||||
|
CacheOperations *prometheus.CounterVec
|
||||||
|
CacheDuration *prometheus.HistogramVec
|
||||||
|
|
||||||
|
// Business logic metrics
|
||||||
|
EthnicOperations *prometheus.CounterVec
|
||||||
|
AuthOperations *prometheus.CounterVec
|
||||||
|
PersonOperations *prometheus.CounterVec
|
||||||
|
|
||||||
|
// Service health metrics
|
||||||
|
ServiceUptime prometheus.Counter
|
||||||
|
ServiceHealthStatus prometheus.Gauge
|
||||||
|
ExternalServiceStatus *prometheus.GaugeVec
|
||||||
|
|
||||||
|
// Resource usage metrics
|
||||||
|
MemoryUsage prometheus.Gauge
|
||||||
|
CPUUsage prometheus.Gauge
|
||||||
|
Goroutines prometheus.Gauge
|
||||||
|
|
||||||
|
// Logger untuk monitoring
|
||||||
|
logger logger.Logger
|
||||||
|
serviceName string
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMetrics creates a new instance of Prometheus metrics
|
||||||
|
func NewMetrics(serviceName string) *Metrics {
|
||||||
|
log := logger.Default().WithFields(
|
||||||
|
logger.String("component", "monitoring"),
|
||||||
|
logger.String("service", serviceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
labels := []string{"method", "endpoint", "status"}
|
||||||
|
dbLabels := []string{"operation", "table", "database"}
|
||||||
|
cacheLabels := []string{"operation", "result"}
|
||||||
|
businessLabels := []string{"operation", "status", "entity"}
|
||||||
|
externalLabels := []string{"service", "status"}
|
||||||
|
|
||||||
|
metrics := &Metrics{
|
||||||
|
serviceName: serviceName,
|
||||||
|
logger: log,
|
||||||
|
startTime: time.Now(),
|
||||||
|
|
||||||
|
// HTTP metrics
|
||||||
|
HttpRequestsTotal: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "http_requests_total",
|
||||||
|
Help: "Total number of HTTP requests",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
labels,
|
||||||
|
),
|
||||||
|
HttpRequestDuration: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "http_request_duration_seconds",
|
||||||
|
Help: "HTTP request duration in seconds",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
labels,
|
||||||
|
),
|
||||||
|
HttpResponseSize: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "http_response_size_bytes",
|
||||||
|
Help: "HTTP response size in bytes",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
Buckets: prometheus.ExponentialBuckets(100, 10, 8),
|
||||||
|
},
|
||||||
|
labels,
|
||||||
|
),
|
||||||
|
HttpRequestsInFlight: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "http_requests_in_flight",
|
||||||
|
Help: "Number of HTTP requests currently being processed",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Database metrics
|
||||||
|
DbConnections: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "db_connections_active",
|
||||||
|
Help: "Number of active database connections",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DbActiveConnections: promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "db_connections_active_by_db",
|
||||||
|
Help: "Number of active database connections by database",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
[]string{"database"},
|
||||||
|
),
|
||||||
|
DbQueryDuration: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "db_query_duration_seconds",
|
||||||
|
Help: "Database query duration in seconds",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
dbLabels,
|
||||||
|
),
|
||||||
|
DbQueryErrors: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "db_query_errors_total",
|
||||||
|
Help: "Total number of database query errors",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
dbLabels,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Cache metrics
|
||||||
|
CacheHits: promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cache_hits_total",
|
||||||
|
Help: "Total number of cache hits",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CacheMisses: promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cache_misses_total",
|
||||||
|
Help: "Total number of cache misses",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CacheOperations: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cache_operations_total",
|
||||||
|
Help: "Total number of cache operations",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
cacheLabels,
|
||||||
|
),
|
||||||
|
CacheDuration: promauto.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "cache_operation_duration_seconds",
|
||||||
|
Help: "Cache operation duration in seconds",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
cacheLabels,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Business logic metrics
|
||||||
|
EthnicOperations: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "ethnic_operations_total",
|
||||||
|
Help: "Total number of ethnic operations",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
businessLabels,
|
||||||
|
),
|
||||||
|
AuthOperations: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "auth_operations_total",
|
||||||
|
Help: "Total number of authentication operations",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
businessLabels,
|
||||||
|
),
|
||||||
|
PersonOperations: promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "person_operations_total",
|
||||||
|
Help: "Total number of person operations",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
businessLabels,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Service health metrics
|
||||||
|
ServiceUptime: promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "service_uptime_seconds",
|
||||||
|
Help: "Service uptime in seconds",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ServiceHealthStatus: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "service_health_status",
|
||||||
|
Help: "Service health status (1 = healthy, 0 = unhealthy)",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ExternalServiceStatus: promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "external_service_status",
|
||||||
|
Help: "External service status (1 = up, 0 = down)",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
externalLabels,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Resource usage metrics
|
||||||
|
MemoryUsage: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "memory_usage_bytes",
|
||||||
|
Help: "Memory usage in bytes",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CPUUsage: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "cpu_usage_percent",
|
||||||
|
Help: "CPU usage percentage",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Goroutines: promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "goroutines_count",
|
||||||
|
Help: "Number of goroutines",
|
||||||
|
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Prometheus metrics initialized successfully",
|
||||||
|
logger.String("service", serviceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordHTTPRequest records HTTP request metrics
|
||||||
|
func (m *Metrics) RecordHTTPRequest(method, endpoint, status string, duration time.Duration, responseSize int64) {
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"method": method,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.HttpRequestsTotal.With(labels).Inc()
|
||||||
|
m.HttpRequestDuration.With(labels).Observe(duration.Seconds())
|
||||||
|
m.HttpResponseSize.With(labels).Observe(float64(responseSize))
|
||||||
|
|
||||||
|
m.logger.Debug("HTTP request recorded",
|
||||||
|
logger.String("method", method),
|
||||||
|
logger.String("endpoint", endpoint),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.Int64("response_size", responseSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordDBQuery records database query metrics
|
||||||
|
func (m *Metrics) RecordDBQuery(operation, table, database string, duration time.Duration, err error) {
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"operation": operation,
|
||||||
|
"table": table,
|
||||||
|
"database": database,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.DbQueryDuration.With(labels).Observe(duration.Seconds())
|
||||||
|
if err != nil {
|
||||||
|
m.DbQueryErrors.With(labels).Inc()
|
||||||
|
m.logger.Error("Database query error recorded",
|
||||||
|
logger.String("operation", operation),
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("database", database),
|
||||||
|
logger.ErrorField(err),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
m.logger.Debug("Database query recorded",
|
||||||
|
logger.String("operation", operation),
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("database", database),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordCacheOperation records cache operation metrics
|
||||||
|
func (m *Metrics) RecordCacheOperation(operation string, hit bool, duration time.Duration) {
|
||||||
|
if hit {
|
||||||
|
m.CacheHits.Inc()
|
||||||
|
} else {
|
||||||
|
m.CacheMisses.Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
result := "hit"
|
||||||
|
if !hit {
|
||||||
|
result = "miss"
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"operation": operation,
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.CacheOperations.With(labels).Inc()
|
||||||
|
m.CacheDuration.With(labels).Observe(duration.Seconds())
|
||||||
|
|
||||||
|
m.logger.Debug("Cache operation recorded",
|
||||||
|
logger.String("operation", operation),
|
||||||
|
logger.String("result", result),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordBusinessOperation records business logic operation metrics
|
||||||
|
func (m *Metrics) RecordBusinessOperation(operationType, operation, status string) {
|
||||||
|
var counter *prometheus.CounterVec
|
||||||
|
|
||||||
|
switch operationType {
|
||||||
|
case "ethnic":
|
||||||
|
counter = m.EthnicOperations
|
||||||
|
case "auth":
|
||||||
|
counter = m.AuthOperations
|
||||||
|
case "person":
|
||||||
|
counter = m.PersonOperations
|
||||||
|
default:
|
||||||
|
m.logger.Warn("Unknown business operation type",
|
||||||
|
logger.String("type", operationType),
|
||||||
|
logger.String("operation", operation),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"operation": operation,
|
||||||
|
"status": status,
|
||||||
|
"entity": operationType,
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.With(labels).Inc()
|
||||||
|
|
||||||
|
m.logger.Debug("Business operation recorded",
|
||||||
|
logger.String("type", operationType),
|
||||||
|
logger.String("operation", operation),
|
||||||
|
logger.String("status", status),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateServiceHealth updates service health status
|
||||||
|
func (m *Metrics) UpdateServiceHealth(healthy bool) {
|
||||||
|
if healthy {
|
||||||
|
m.ServiceHealthStatus.Set(1)
|
||||||
|
m.logger.Info("Service health status updated to healthy")
|
||||||
|
} else {
|
||||||
|
m.ServiceHealthStatus.Set(0)
|
||||||
|
m.logger.Error("Service health status updated to unhealthy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExternalServiceStatus updates external service status
|
||||||
|
func (m *Metrics) UpdateExternalServiceStatus(service string, status bool) {
|
||||||
|
value := 0.0
|
||||||
|
statusStr := "down"
|
||||||
|
if status {
|
||||||
|
value = 1.0
|
||||||
|
statusStr = "up"
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"service": service,
|
||||||
|
"status": statusStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.ExternalServiceStatus.With(labels).Set(value)
|
||||||
|
|
||||||
|
m.logger.Info("External service status updated",
|
||||||
|
logger.String("service", service),
|
||||||
|
logger.String("status", statusStr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateResourceUsage updates resource usage metrics
|
||||||
|
func (m *Metrics) UpdateResourceUsage(memory, cpu float64, goroutines int) {
|
||||||
|
m.MemoryUsage.Set(memory)
|
||||||
|
m.CPUUsage.Set(cpu)
|
||||||
|
m.Goroutines.Set(float64(goroutines))
|
||||||
|
|
||||||
|
m.logger.Debug("Resource usage updated",
|
||||||
|
logger.Float64("memory_mb", memory),
|
||||||
|
logger.Float64("cpu_percent", cpu),
|
||||||
|
logger.Int("goroutines", goroutines),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementUptime increments service uptime
|
||||||
|
func (m *Metrics) IncrementUptime(seconds float64) {
|
||||||
|
m.ServiceUptime.Add(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUptime returns current uptime in seconds
|
||||||
|
func (m *Metrics) GetUptime() float64 {
|
||||||
|
return time.Since(m.startTime).Seconds()
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/middleware.go
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetricsMiddleware creates Gin middleware for collecting HTTP metrics
|
||||||
|
func MetricsMiddleware(metrics *Metrics) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Generate request ID if not present
|
||||||
|
requestID := c.GetHeader("X-Request-ID")
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = uuid.New().String()
|
||||||
|
c.Header("X-Request-ID", requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add request ID to context for logging
|
||||||
|
log := logger.Default().WithFields(
|
||||||
|
logger.String("request_id", requestID),
|
||||||
|
logger.String("method", c.Request.Method),
|
||||||
|
logger.String("path", c.Request.URL.Path),
|
||||||
|
logger.String("client_ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start timer
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Increment requests in flight
|
||||||
|
metrics.HttpRequestsInFlight.Inc()
|
||||||
|
defer metrics.HttpRequestsInFlight.Dec()
|
||||||
|
|
||||||
|
// Log request start
|
||||||
|
log.Info("HTTP request started")
|
||||||
|
|
||||||
|
// Process request
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
// Get response size
|
||||||
|
responseSize := c.Writer.Size()
|
||||||
|
if responseSize < 0 {
|
||||||
|
responseSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status code
|
||||||
|
status := strconv.Itoa(c.Writer.Status())
|
||||||
|
|
||||||
|
// Record metrics
|
||||||
|
metrics.RecordHTTPRequest(
|
||||||
|
c.Request.Method,
|
||||||
|
c.FullPath(),
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
int64(responseSize),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log request completion
|
||||||
|
log.Info("HTTP request completed",
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.Int("response_size", responseSize),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add request ID to response
|
||||||
|
c.Header("X-Request-ID", requestID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseMetricsMiddleware creates middleware for database metrics
|
||||||
|
func DatabaseMetricsMiddleware(metrics *Metrics) func(operation, table, database string) func(error) {
|
||||||
|
return func(operation, table, database string) func(error) {
|
||||||
|
start := time.Now()
|
||||||
|
return func(err error) {
|
||||||
|
duration := time.Since(start)
|
||||||
|
metrics.RecordDBQuery(operation, table, database, duration, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheMetricsMiddleware creates middleware for cache metrics
|
||||||
|
func CacheMetricsMiddleware(metrics *Metrics) func(operation string, hit bool) func() {
|
||||||
|
return func(operation string, hit bool) func() {
|
||||||
|
start := time.Now()
|
||||||
|
return func() {
|
||||||
|
duration := time.Since(start)
|
||||||
|
metrics.RecordCacheOperation(operation, hit, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/repository_wrapper.go
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service/pkg/logger"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GormDBWrapper wraps GORM DB with metrics collection
|
||||||
|
type GormDBWrapper struct {
|
||||||
|
*gorm.DB
|
||||||
|
metrics *Metrics
|
||||||
|
database string
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGormDBWrapper creates a new GORM wrapper with metrics
|
||||||
|
func NewGormDBWrapper(db *gorm.DB, metrics *Metrics, database string) *GormDBWrapper {
|
||||||
|
log := logger.Default().WithFields(
|
||||||
|
logger.String("component", "gorm_wrapper"),
|
||||||
|
logger.String("database", database),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &GormDBWrapper{
|
||||||
|
DB: db,
|
||||||
|
metrics: metrics,
|
||||||
|
database: database,
|
||||||
|
logger: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create overrides GORM Create with metrics
|
||||||
|
func (w *GormDBWrapper) Create(value interface{}) *gorm.DB {
|
||||||
|
start := time.Now()
|
||||||
|
result := w.DB.Create(value)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
table := w.DB.Statement.Table
|
||||||
|
status := "success"
|
||||||
|
if result.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metrics.RecordDBQuery("create", table, w.database, duration, result.Error)
|
||||||
|
|
||||||
|
w.logger.Debug("Database create operation",
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.ErrorField(result.Error),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find overrides GORM Find with metrics
|
||||||
|
func (w *GormDBWrapper) Find(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||||
|
start := time.Now()
|
||||||
|
result := w.DB.Find(dest, conds...)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
table := w.DB.Statement.Table
|
||||||
|
status := "success"
|
||||||
|
if result.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metrics.RecordDBQuery("find", table, w.database, duration, result.Error)
|
||||||
|
|
||||||
|
w.logger.Debug("Database find operation",
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.ErrorField(result.Error),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// First overrides GORM First with metrics
|
||||||
|
func (w *GormDBWrapper) First(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||||
|
start := time.Now()
|
||||||
|
result := w.DB.First(dest, conds...)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
table := w.DB.Statement.Table
|
||||||
|
status := "success"
|
||||||
|
if result.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metrics.RecordDBQuery("first", table, w.database, duration, result.Error)
|
||||||
|
|
||||||
|
w.logger.Debug("Database first operation",
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.ErrorField(result.Error),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overrides GORM Update with metrics
|
||||||
|
func (w *GormDBWrapper) Update(column string, value interface{}) *gorm.DB {
|
||||||
|
start := time.Now()
|
||||||
|
result := w.DB.Update(column, value)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
table := w.DB.Statement.Table
|
||||||
|
status := "success"
|
||||||
|
if result.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metrics.RecordDBQuery("update", table, w.database, duration, result.Error)
|
||||||
|
|
||||||
|
w.logger.Debug("Database update operation",
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.ErrorField(result.Error),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete overrides GORM Delete with metrics
|
||||||
|
func (w *GormDBWrapper) Delete(value interface{}, conds ...interface{}) *gorm.DB {
|
||||||
|
start := time.Now()
|
||||||
|
result := w.DB.Delete(value, conds...)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
table := w.DB.Statement.Table
|
||||||
|
status := "success"
|
||||||
|
if result.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metrics.RecordDBQuery("delete", table, w.database, duration, result.Error)
|
||||||
|
|
||||||
|
w.logger.Debug("Database delete operation",
|
||||||
|
logger.String("table", table),
|
||||||
|
logger.String("status", status),
|
||||||
|
logger.Float64("duration_seconds", duration.Seconds()),
|
||||||
|
logger.ErrorField(result.Error),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext overrides GORM WithContext to maintain wrapper
|
||||||
|
func (w *GormDBWrapper) WithContext(ctx context.Context) *GormDBWrapper {
|
||||||
|
return &GormDBWrapper{
|
||||||
|
DB: w.DB.WithContext(ctx),
|
||||||
|
metrics: w.metrics,
|
||||||
|
database: w.database,
|
||||||
|
logger: w.logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model overrides GORM Model to maintain wrapper
|
||||||
|
func (w *GormDBWrapper) Model(value interface{}) *GormDBWrapper {
|
||||||
|
return &GormDBWrapper{
|
||||||
|
DB: w.DB.Model(value),
|
||||||
|
metrics: w.metrics,
|
||||||
|
database: w.database,
|
||||||
|
logger: w.logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table overrides GORM Table to maintain wrapper
|
||||||
|
func (w *GormDBWrapper) Table(name string, args ...interface{}) *GormDBWrapper {
|
||||||
|
return &GormDBWrapper{
|
||||||
|
DB: w.DB.Table(name, args...),
|
||||||
|
metrics: w.metrics,
|
||||||
|
database: w.database,
|
||||||
|
logger: w.logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v3.12.4
|
||||||
|
// source: internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto
|
||||||
|
|
||||||
|
package permissionV1
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
|
||||||
|
wrappers "github.com/golang/protobuf/ptypes/wrappers"
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pesan request untuk RPC GetRolePermissionTree.
|
||||||
|
type GetRolePermissionTreeRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
RoleKeycloak string `protobuf:"bytes,1,opt,name=role_keycloak,json=roleKeycloak,proto3" json:"role_keycloak,omitempty"`
|
||||||
|
GroupKeycloak []string `protobuf:"bytes,2,rep,name=group_keycloak,json=groupKeycloak,proto3" json:"group_keycloak,omitempty"`
|
||||||
|
// Menggunakan wrapper BoolValue untuk membedakan antara 'false' dan 'tidak diset'.
|
||||||
|
ActiveOnly *wrappers.BoolValue `protobuf:"bytes,3,opt,name=active_only,json=activeOnly,proto3" json:"active_only,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) Reset() {
|
||||||
|
*x = GetRolePermissionTreeRequest{}
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetRolePermissionTreeRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetRolePermissionTreeRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetRolePermissionTreeRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) GetRoleKeycloak() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RoleKeycloak
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) GetGroupKeycloak() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.GroupKeycloak
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRolePermissionTreeRequest) GetActiveOnly() *wrappers.BoolValue {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActiveOnly
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pesan response untuk RPC GetRolePermissionTree.
|
||||||
|
type RolePermissionTreeResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
|
||||||
|
Data *RolePermissionData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionTreeResponse) Reset() {
|
||||||
|
*x = RolePermissionTreeResponse{}
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionTreeResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RolePermissionTreeResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RolePermissionTreeResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RolePermissionTreeResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RolePermissionTreeResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionTreeResponse) GetSuccess() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Success
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionTreeResponse) GetData() *RolePermissionData {
|
||||||
|
if x != nil {
|
||||||
|
return x.Data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolePermissionData struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"`
|
||||||
|
Group []string `protobuf:"bytes,2,rep,name=group,proto3" json:"group,omitempty"`
|
||||||
|
Access []*AccessTreeItem `protobuf:"bytes,3,rep,name=access,proto3" json:"access,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) Reset() {
|
||||||
|
*x = RolePermissionData{}
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RolePermissionData) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RolePermissionData.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RolePermissionData) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) GetRole() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Role
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) GetGroup() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Group
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RolePermissionData) GetAccess() []*AccessTreeItem {
|
||||||
|
if x != nil {
|
||||||
|
return x.Access
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessTreeItem struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||||
|
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
|
||||||
|
Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"`
|
||||||
|
Sort int32 `protobuf:"varint,6,opt,name=sort,proto3" json:"sort,omitempty"`
|
||||||
|
Active bool `protobuf:"varint,7,opt,name=active,proto3" json:"active,omitempty"`
|
||||||
|
Permission *Permission `protobuf:"bytes,8,opt,name=permission,proto3,oneof" json:"permission,omitempty"`
|
||||||
|
Children []*AccessTreeItem `protobuf:"bytes,9,rep,name=children,proto3" json:"children,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) Reset() {
|
||||||
|
*x = AccessTreeItem{}
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*AccessTreeItem) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use AccessTreeItem.ProtoReflect.Descriptor instead.
|
||||||
|
func (*AccessTreeItem) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetIcon() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Icon
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetUrl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Url
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetLevel() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Level
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetSort() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Sort
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetActive() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Active
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetPermission() *Permission {
|
||||||
|
if x != nil {
|
||||||
|
return x.Permission
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AccessTreeItem) GetChildren() []*AccessTreeItem {
|
||||||
|
if x != nil {
|
||||||
|
return x.Children
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Create bool `protobuf:"varint,1,opt,name=create,proto3" json:"create,omitempty"`
|
||||||
|
Read bool `protobuf:"varint,2,opt,name=read,proto3" json:"read,omitempty"`
|
||||||
|
Update bool `protobuf:"varint,3,opt,name=update,proto3" json:"update,omitempty"`
|
||||||
|
Delete bool `protobuf:"varint,4,opt,name=delete,proto3" json:"delete,omitempty"`
|
||||||
|
Disable bool `protobuf:"varint,5,opt,name=disable,proto3" json:"disable,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) Reset() {
|
||||||
|
*x = Permission{}
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Permission) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Permission) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Permission.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Permission) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) GetCreate() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Create
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) GetRead() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Read
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) GetUpdate() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Update
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) GetDelete() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Delete
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Permission) GetDisable() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Disable
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"Kinternal/infrastructure/transport/grpc/proto/permission/v1/permission.proto\x12\rpermission.v1\x1a\x1egoogle/protobuf/wrappers.proto\"\xa7\x01\n" +
|
||||||
|
"\x1cGetRolePermissionTreeRequest\x12#\n" +
|
||||||
|
"\rrole_keycloak\x18\x01 \x01(\tR\froleKeycloak\x12%\n" +
|
||||||
|
"\x0egroup_keycloak\x18\x02 \x03(\tR\rgroupKeycloak\x12;\n" +
|
||||||
|
"\vactive_only\x18\x03 \x01(\v2\x1a.google.protobuf.BoolValueR\n" +
|
||||||
|
"activeOnly\"m\n" +
|
||||||
|
"\x1aRolePermissionTreeResponse\x12\x18\n" +
|
||||||
|
"\asuccess\x18\x01 \x01(\bR\asuccess\x125\n" +
|
||||||
|
"\x04data\x18\x02 \x01(\v2!.permission.v1.RolePermissionDataR\x04data\"u\n" +
|
||||||
|
"\x12RolePermissionData\x12\x12\n" +
|
||||||
|
"\x04role\x18\x01 \x01(\tR\x04role\x12\x14\n" +
|
||||||
|
"\x05group\x18\x02 \x03(\tR\x05group\x125\n" +
|
||||||
|
"\x06access\x18\x03 \x03(\v2\x1d.permission.v1.AccessTreeItemR\x06access\"\xa6\x02\n" +
|
||||||
|
"\x0eAccessTreeItem\x12\x0e\n" +
|
||||||
|
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
|
||||||
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||||
|
"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x10\n" +
|
||||||
|
"\x03url\x18\x04 \x01(\tR\x03url\x12\x14\n" +
|
||||||
|
"\x05level\x18\x05 \x01(\x05R\x05level\x12\x12\n" +
|
||||||
|
"\x04sort\x18\x06 \x01(\x05R\x04sort\x12\x16\n" +
|
||||||
|
"\x06active\x18\a \x01(\bR\x06active\x12>\n" +
|
||||||
|
"\n" +
|
||||||
|
"permission\x18\b \x01(\v2\x19.permission.v1.PermissionH\x00R\n" +
|
||||||
|
"permission\x88\x01\x01\x129\n" +
|
||||||
|
"\bchildren\x18\t \x03(\v2\x1d.permission.v1.AccessTreeItemR\bchildrenB\r\n" +
|
||||||
|
"\v_permission\"\x82\x01\n" +
|
||||||
|
"\n" +
|
||||||
|
"Permission\x12\x16\n" +
|
||||||
|
"\x06create\x18\x01 \x01(\bR\x06create\x12\x12\n" +
|
||||||
|
"\x04read\x18\x02 \x01(\bR\x04read\x12\x16\n" +
|
||||||
|
"\x06update\x18\x03 \x01(\bR\x06update\x12\x16\n" +
|
||||||
|
"\x06delete\x18\x04 \x01(\bR\x06delete\x12\x18\n" +
|
||||||
|
"\adisable\x18\x05 \x01(\bR\adisable2\x87\x01\n" +
|
||||||
|
"\x14RolPermissionService\x12o\n" +
|
||||||
|
"\x15GetRolePermissionTree\x12+.permission.v1.GetRolePermissionTreeRequest\x1a).permission.v1.RolePermissionTreeResponseBGZEinternal/infrastructure/transport/grpc/gen/permission/v1;permissionV1b\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescOnce sync.Once
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescGZIP() []byte {
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescOnce.Do(func() {
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc), len(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||||
|
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes = []any{
|
||||||
|
(*GetRolePermissionTreeRequest)(nil), // 0: permission.v1.GetRolePermissionTreeRequest
|
||||||
|
(*RolePermissionTreeResponse)(nil), // 1: permission.v1.RolePermissionTreeResponse
|
||||||
|
(*RolePermissionData)(nil), // 2: permission.v1.RolePermissionData
|
||||||
|
(*AccessTreeItem)(nil), // 3: permission.v1.AccessTreeItem
|
||||||
|
(*Permission)(nil), // 4: permission.v1.Permission
|
||||||
|
(*wrappers.BoolValue)(nil), // 5: google.protobuf.BoolValue
|
||||||
|
}
|
||||||
|
var file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs = []int32{
|
||||||
|
5, // 0: permission.v1.GetRolePermissionTreeRequest.active_only:type_name -> google.protobuf.BoolValue
|
||||||
|
2, // 1: permission.v1.RolePermissionTreeResponse.data:type_name -> permission.v1.RolePermissionData
|
||||||
|
3, // 2: permission.v1.RolePermissionData.access:type_name -> permission.v1.AccessTreeItem
|
||||||
|
4, // 3: permission.v1.AccessTreeItem.permission:type_name -> permission.v1.Permission
|
||||||
|
3, // 4: permission.v1.AccessTreeItem.children:type_name -> permission.v1.AccessTreeItem
|
||||||
|
0, // 5: permission.v1.RolPermissionService.GetRolePermissionTree:input_type -> permission.v1.GetRolePermissionTreeRequest
|
||||||
|
1, // 6: permission.v1.RolPermissionService.GetRolePermissionTree:output_type -> permission.v1.RolePermissionTreeResponse
|
||||||
|
6, // [6:7] is the sub-list for method output_type
|
||||||
|
5, // [5:6] is the sub-list for method input_type
|
||||||
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
|
0, // [0:5] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_init() }
|
||||||
|
func file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_init() {
|
||||||
|
if File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes[3].OneofWrappers = []any{}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc), len(file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 5,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes,
|
||||||
|
DependencyIndexes: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs,
|
||||||
|
MessageInfos: file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto = out.File
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_goTypes = nil
|
||||||
|
file_internal_infrastructure_transport_grpc_proto_permission_v1_permission_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.0
|
||||||
|
// - protoc v3.12.4
|
||||||
|
// source: internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto
|
||||||
|
|
||||||
|
package permissionV1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
RolPermissionService_GetRolePermissionTree_FullMethodName = "/permission.v1.RolPermissionService/GetRolePermissionTree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RolPermissionServiceClient is the client API for RolPermissionService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// Definisi service.
|
||||||
|
type RolPermissionServiceClient interface {
|
||||||
|
// Mengambil permission tree untuk role dan/atau group tertentu.
|
||||||
|
GetRolePermissionTree(ctx context.Context, in *GetRolePermissionTreeRequest, opts ...grpc.CallOption) (*RolePermissionTreeResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rolPermissionServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRolPermissionServiceClient(cc grpc.ClientConnInterface) RolPermissionServiceClient {
|
||||||
|
return &rolPermissionServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rolPermissionServiceClient) GetRolePermissionTree(ctx context.Context, in *GetRolePermissionTreeRequest, opts ...grpc.CallOption) (*RolePermissionTreeResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RolePermissionTreeResponse)
|
||||||
|
err := c.cc.Invoke(ctx, RolPermissionService_GetRolePermissionTree_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolPermissionServiceServer is the server API for RolPermissionService service.
|
||||||
|
// All implementations must embed UnimplementedRolPermissionServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// Definisi service.
|
||||||
|
type RolPermissionServiceServer interface {
|
||||||
|
// Mengambil permission tree untuk role dan/atau group tertentu.
|
||||||
|
GetRolePermissionTree(context.Context, *GetRolePermissionTreeRequest) (*RolePermissionTreeResponse, error)
|
||||||
|
mustEmbedUnimplementedRolPermissionServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedRolPermissionServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedRolPermissionServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedRolPermissionServiceServer) GetRolePermissionTree(context.Context, *GetRolePermissionTreeRequest) (*RolePermissionTreeResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetRolePermissionTree not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedRolPermissionServiceServer) mustEmbedUnimplementedRolPermissionServiceServer() {}
|
||||||
|
func (UnimplementedRolPermissionServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeRolPermissionServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to RolPermissionServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeRolPermissionServiceServer interface {
|
||||||
|
mustEmbedUnimplementedRolPermissionServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterRolPermissionServiceServer(s grpc.ServiceRegistrar, srv RolPermissionServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedRolPermissionServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&RolPermissionService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _RolPermissionService_GetRolePermissionTree_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetRolePermissionTreeRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RolPermissionServiceServer).GetRolePermissionTree(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: RolPermissionService_GetRolePermissionTree_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(RolPermissionServiceServer).GetRolePermissionTree(ctx, req.(*GetRolePermissionTreeRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolPermissionService_ServiceDesc is the grpc.ServiceDesc for RolPermissionService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var RolPermissionService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "permission.v1.RolPermissionService",
|
||||||
|
HandlerType: (*RolPermissionServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "GetRolePermissionTree",
|
||||||
|
Handler: _RolPermissionService_GetRolePermissionTree_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "internal/infrastructure/transport/grpc/proto/permission/v1/permission.proto",
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"service/pkg/errors"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// / handleServiceError mengkonversi error dari service layer ke gRPC status error
|
||||||
|
func handleServiceError(err error) error {
|
||||||
|
appErr := errors.FromError(err)
|
||||||
|
if appErr == nil {
|
||||||
|
return status.Error(codes.Internal, "internal server error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping kategori error ke gRPC codes
|
||||||
|
switch appErr.Category() {
|
||||||
|
case errors.CategoryValidation:
|
||||||
|
return status.Error(codes.InvalidArgument, appErr.Error())
|
||||||
|
case errors.CategoryNotFound:
|
||||||
|
return status.Error(codes.NotFound, appErr.Error())
|
||||||
|
case errors.CategoryConflict:
|
||||||
|
return status.Error(codes.AlreadyExists, appErr.Error())
|
||||||
|
case errors.CategoryUnauthorized:
|
||||||
|
return status.Error(codes.Unauthenticated, appErr.Error())
|
||||||
|
case errors.CategoryForbidden:
|
||||||
|
return status.Error(codes.PermissionDenied, appErr.Error())
|
||||||
|
default:
|
||||||
|
return status.Error(codes.Internal, appErr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user