first commit
This commit is contained in:
No files matched your search
@@ -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())
|
||||
}
|
||||
}
|
||||
Loaded 100 of 443 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in New Issue
Block a user