initiate repo
This commit is contained in:
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/server"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func gracefulShutdown(apiServer *http.Server, done chan bool) {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
log.Println("Shutting down gracefully, press Ctrl+C again to force")
|
||||||
|
stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := apiServer.Shutdown(ctx); err != nil {
|
||||||
|
log.Printf("Server forced to shutdown with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Server exiting")
|
||||||
|
|
||||||
|
done <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Starting API Service...")
|
||||||
|
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Printf("Warning: .env file not found or could not be loaded: %v", err)
|
||||||
|
log.Println("Continuing with system environment variables...")
|
||||||
|
}
|
||||||
|
|
||||||
|
server := server.NewServer()
|
||||||
|
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
|
||||||
|
// Run graceful shutdown in a separate goroutine
|
||||||
|
go gracefulShutdown(server, done)
|
||||||
|
|
||||||
|
log.Printf("Server starting on port %s", server.Addr)
|
||||||
|
err := server.ListenAndServe()
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
panic(fmt.Sprintf("http server error: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the graceful shutdown to complete
|
||||||
|
<-done
|
||||||
|
log.Println("Graceful shutdown complete.")
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: antrian_operasi
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: rssa
|
||||||
|
POSTGRES_PASSWORD: supersecret
|
||||||
|
POSTGRES_DB: antrian_operasi
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U rssa -d antrian_operasi"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
module antrian-operasi
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.11.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.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/pgx/v5 v5.8.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.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/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // 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.mongodb.org/mongo-driver v1.17.7 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||||
|
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||||
|
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
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/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||||
|
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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
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/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
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/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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
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/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.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/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/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.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
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/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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfig() *Config {
|
||||||
|
errLoadEnv := godotenv.Load()
|
||||||
|
if errLoadEnv != nil {
|
||||||
|
log.Println("error loading .env")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
Port: getEnvAsInt("PORT", 8080),
|
||||||
|
Mode: getEnv("GIN_MODE", "debug"),
|
||||||
|
},
|
||||||
|
Databases: make(map[string]DatabaseConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
config.loadCustomDatabaseConfigs()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) loadCustomDatabaseConfigs() {
|
||||||
|
envVars := os.Environ()
|
||||||
|
dbConfigs := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
// Parse database configurations from environment variables
|
||||||
|
for _, envVar := range envVars {
|
||||||
|
parts := strings.SplitN(envVar, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := parts[0]
|
||||||
|
value := parts[1]
|
||||||
|
|
||||||
|
// Parse specific database configurations
|
||||||
|
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
||||||
|
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
||||||
|
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
||||||
|
strings.HasSuffix(key, "_NAME") {
|
||||||
|
|
||||||
|
segments := strings.Split(key, "_")
|
||||||
|
if len(segments) >= 2 {
|
||||||
|
dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_"))
|
||||||
|
property := strings.ToLower(segments[len(segments)-1])
|
||||||
|
|
||||||
|
if dbConfigs[dbName] == nil {
|
||||||
|
dbConfigs[dbName] = make(map[string]string)
|
||||||
|
}
|
||||||
|
dbConfigs[dbName][property] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DatabaseConfig from parsed configurations for additional databases
|
||||||
|
for name, config := range dbConfigs {
|
||||||
|
// Skip empty configurations or system configurations
|
||||||
|
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dbType := getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres"))
|
||||||
|
|
||||||
|
// Skip if username is empty and it's not a system config
|
||||||
|
username := getEnvFromMap(config, "username", "")
|
||||||
|
if username == "" && !strings.HasPrefix(name, "chrome") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConfig := DatabaseConfig{
|
||||||
|
Name: name,
|
||||||
|
Type: dbType,
|
||||||
|
Host: getEnvFromMap(config, "host", "localhost"),
|
||||||
|
Port: getEnvAsIntFromMap(config, "port", getDefaultPort(dbType)),
|
||||||
|
Username: username,
|
||||||
|
Password: getEnvFromMap(config, "password", ""),
|
||||||
|
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
||||||
|
Schema: getEnvFromMap(config, "schema", getDefaultSchema(dbType)),
|
||||||
|
SSLMode: getEnvFromMap(config, "sslmode", getDefaultSSLMode(dbType)),
|
||||||
|
Path: getEnvFromMap(config, "path", ""),
|
||||||
|
Options: getEnvFromMap(config, "options", ""),
|
||||||
|
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", getDefaultMaxOpenConns(dbType)),
|
||||||
|
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", getDefaultMaxIdleConns(dbType)),
|
||||||
|
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", getDefaultConnMaxLifetime(dbType))),
|
||||||
|
// Security settings
|
||||||
|
RequireSSL: getEnvAsBoolFromMap(config, "require_ssl", false),
|
||||||
|
SSLRootCert: getEnvFromMap(config, "ssl_root_cert", ""),
|
||||||
|
SSLCert: getEnvFromMap(config, "ssl_cert", ""),
|
||||||
|
SSLKey: getEnvFromMap(config, "ssl_key", ""),
|
||||||
|
Timeout: parseDuration(getEnvFromMap(config, "timeout", "30s")),
|
||||||
|
ConnectTimeout: parseDuration(getEnvFromMap(config, "connect_timeout", "10s")),
|
||||||
|
ReadTimeout: parseDuration(getEnvFromMap(config, "read_timeout", "30s")),
|
||||||
|
WriteTimeout: parseDuration(getEnvFromMap(config, "write_timeout", "30s")),
|
||||||
|
StatementTimeout: parseDuration(getEnvFromMap(config, "statement_timeout", "360s")),
|
||||||
|
// Connection pool settings
|
||||||
|
MaxLifetime: parseDuration(getEnvFromMap(config, "max_lifetime", "1h")),
|
||||||
|
MaxIdleTime: parseDuration(getEnvFromMap(config, "max_idle_time", "5m")),
|
||||||
|
HealthCheckPeriod: parseDuration(getEnvFromMap(config, "health_check_period", "1m")),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Databases[name] = dbConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvAsInt(key string, defaultValue int) int {
|
||||||
|
valueStr := getEnv(key, "")
|
||||||
|
if value, err := strconv.Atoi(valueStr); err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvFromMap(config map[string]string, key, defaultValue string) string {
|
||||||
|
if value, exists := config[key]; exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvAsIntFromMap(config map[string]string, key string, defaultValue int) int {
|
||||||
|
if value, exists := config[key]; exists {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvAsBoolFromMap(config map[string]string, key string, defaultValue bool) bool {
|
||||||
|
if value, exists := config[key]; exists {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for getting default values based on database type
|
||||||
|
func getDefaultPort(dbType string) int {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return 5432
|
||||||
|
case "mysql":
|
||||||
|
return 3306
|
||||||
|
case "sqlserver":
|
||||||
|
return 1433
|
||||||
|
case "mongodb":
|
||||||
|
return 27017
|
||||||
|
case "sqlite":
|
||||||
|
return 0 // SQLite doesn't use port
|
||||||
|
default:
|
||||||
|
return 5432
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultSchema(dbType string) string {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return "public"
|
||||||
|
case "mysql":
|
||||||
|
return ""
|
||||||
|
case "sqlserver":
|
||||||
|
return "dbo"
|
||||||
|
case "mongodb":
|
||||||
|
return ""
|
||||||
|
case "sqlite":
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultSSLMode(dbType string) string {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return "disable"
|
||||||
|
case "mysql":
|
||||||
|
return "false"
|
||||||
|
case "sqlserver":
|
||||||
|
return "false"
|
||||||
|
case "mongodb":
|
||||||
|
return "false"
|
||||||
|
case "sqlite":
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return "disable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultMaxOpenConns(dbType string) int {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return 25
|
||||||
|
case "mysql":
|
||||||
|
return 25
|
||||||
|
case "sqlserver":
|
||||||
|
return 25
|
||||||
|
case "mongodb":
|
||||||
|
return 100
|
||||||
|
case "sqlite":
|
||||||
|
return 1 // SQLite only supports one writer at a time
|
||||||
|
default:
|
||||||
|
return 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultMaxIdleConns(dbType string) int {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return 25
|
||||||
|
case "mysql":
|
||||||
|
return 25
|
||||||
|
case "sqlserver":
|
||||||
|
return 25
|
||||||
|
case "mongodb":
|
||||||
|
return 10
|
||||||
|
case "sqlite":
|
||||||
|
return 1 // SQLite only supports one writer at a time
|
||||||
|
default:
|
||||||
|
return 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultConnMaxLifetime(dbType string) string {
|
||||||
|
switch dbType {
|
||||||
|
case "postgres":
|
||||||
|
return "5m"
|
||||||
|
case "mysql":
|
||||||
|
return "5m"
|
||||||
|
case "sqlserver":
|
||||||
|
return "5m"
|
||||||
|
case "mongodb":
|
||||||
|
return "30m"
|
||||||
|
case "sqlite":
|
||||||
|
return "5m"
|
||||||
|
default:
|
||||||
|
return "5m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDuration(durationStr string) time.Duration {
|
||||||
|
if duration, err := time.ParseDuration(durationStr); err == nil {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
return 5 * time.Minute
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig
|
||||||
|
Databases map[string]DatabaseConfig
|
||||||
|
ReadReplicas map[string][]DatabaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port int
|
||||||
|
Mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Name string
|
||||||
|
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Database string
|
||||||
|
Schema string
|
||||||
|
SSLMode string
|
||||||
|
Path string // For SQLite
|
||||||
|
Options string // Additional connection options
|
||||||
|
MaxOpenConns int // Max open connections
|
||||||
|
MaxIdleConns int // Max idle connections
|
||||||
|
ConnMaxLifetime time.Duration // Connection max lifetime
|
||||||
|
// Security settings
|
||||||
|
RequireSSL bool // Require SSL connection
|
||||||
|
SSLRootCert string // Path to SSL root certificate
|
||||||
|
SSLCert string // Path to SSL client certificate
|
||||||
|
SSLKey string // Path to SSL client key
|
||||||
|
Timeout time.Duration // Connection timeout
|
||||||
|
ConnectTimeout time.Duration // Connect timeout
|
||||||
|
ReadTimeout time.Duration // Read timeout
|
||||||
|
WriteTimeout time.Duration // Write timeout
|
||||||
|
StatementTimeout time.Duration // Statement timeout for PostgreSQL
|
||||||
|
// Connection pool settings
|
||||||
|
MaxLifetime time.Duration // Maximum amount of time a connection may be reused
|
||||||
|
MaxIdleTime time.Duration // Maximum amount of time a connection may be idle
|
||||||
|
HealthCheckPeriod time.Duration // Health check period
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DatabaseType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Postgres DatabaseType = "postgres"
|
||||||
|
MySQL DatabaseType = "mysql"
|
||||||
|
SQLServer DatabaseType = "sqlserver"
|
||||||
|
SQLite DatabaseType = "sqlite"
|
||||||
|
MongoDB DatabaseType = "mongodb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Health() map[string]map[string]string
|
||||||
|
GetDB(name string) (*sql.DB, error)
|
||||||
|
GetSQLXDB(name string) (*sqlx.DB, error) // Tambahkan metode ini
|
||||||
|
GetMongoClient(name string) (*mongo.Client, error)
|
||||||
|
GetReadDB(name string) (*sql.DB, error)
|
||||||
|
Close() error
|
||||||
|
ListDBs() []string
|
||||||
|
GetDBType(name string) (DatabaseType, error)
|
||||||
|
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error
|
||||||
|
NotifyChange(dbName, channel, payload string) error
|
||||||
|
GetPrimaryDB(name string) (*sql.DB, error)
|
||||||
|
ExecuteQuery(ctx context.Context, dbName string, query string, args ...interface{}) (*sql.Rows, error)
|
||||||
|
ExecuteQueryRow(ctx context.Context, dbName string, query string, args ...interface{}) *sql.Row
|
||||||
|
Exec(ctx context.Context, dbName string, query string, args ...interface{}) (sql.Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
sqlDatabases map[string]*sql.DB
|
||||||
|
sqlxDatabases map[string]*sqlx.DB // Tambahkan map untuk 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
|
||||||
|
listeners map[string]*pq.Listener
|
||||||
|
listenersMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbManager *service
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) loadFromConfig(cfg *config.Config) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// Load primary databases
|
||||||
|
for name, dbConfig := range cfg.Databases {
|
||||||
|
fmt.Printf("db name : %s", name)
|
||||||
|
s.configs[name] = dbConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load read replicas
|
||||||
|
for name, replicaConfigs := range cfg.ReadReplicas {
|
||||||
|
s.readConfigs[name] = replicaConfigs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) addDatabase(name string, config config.DatabaseConfig) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dbType := DatabaseType(config.Type)
|
||||||
|
log.Printf("Database type: %s", dbType)
|
||||||
|
|
||||||
|
switch dbType {
|
||||||
|
case Postgres:
|
||||||
|
db, err = s.openPostgresConnection(config)
|
||||||
|
case MySQL:
|
||||||
|
db, err = s.openMySQLConnection(config)
|
||||||
|
case SQLServer:
|
||||||
|
db, err = s.openSQLServerConnection(config)
|
||||||
|
case SQLite:
|
||||||
|
db, err = s.openSQLiteConnection(config)
|
||||||
|
case MongoDB:
|
||||||
|
return s.addMongoDB(name, config)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database type: %s", config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Error connecting to database %s: %v", name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ Successfully connected to database: %s", name)
|
||||||
|
return s.configureSQLDB(name, db, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) addReadReplica(name string, index int, config config.DatabaseConfig) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dbType := DatabaseType(config.Type)
|
||||||
|
|
||||||
|
switch dbType {
|
||||||
|
case Postgres:
|
||||||
|
db, err = s.openPostgresConnection(config)
|
||||||
|
case MySQL:
|
||||||
|
db, err = s.openMySQLConnection(config)
|
||||||
|
case SQLServer:
|
||||||
|
db, err = s.openSQLServerConnection(config)
|
||||||
|
case SQLite:
|
||||||
|
db, err = s.openSQLiteConnection(config)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database type for read replica: %s", config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.readReplicas[name] == nil {
|
||||||
|
s.readReplicas[name] = make([]*sql.DB, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have enough slots
|
||||||
|
for len(s.readReplicas[name]) <= index {
|
||||||
|
s.readReplicas[name] = append(s.readReplicas[name], nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.readReplicas[name][index] = db
|
||||||
|
log.Printf("Successfully connected to read replica %s[%d]", name, index)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config) Service {
|
||||||
|
once.Do(func() {
|
||||||
|
dbManager = &service{
|
||||||
|
sqlDatabases: make(map[string]*sql.DB),
|
||||||
|
sqlxDatabases: make(map[string]*sqlx.DB), // Inisialisasi map sqlx
|
||||||
|
mongoClients: make(map[string]*mongo.Client),
|
||||||
|
readReplicas: make(map[string][]*sql.DB),
|
||||||
|
configs: make(map[string]config.DatabaseConfig),
|
||||||
|
readConfigs: make(map[string][]config.DatabaseConfig),
|
||||||
|
readBalancer: make(map[string]int),
|
||||||
|
listeners: make(map[string]*pq.Listener),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Initializing database service...")
|
||||||
|
dbManager.loadFromConfig(cfg)
|
||||||
|
fmt.Printf("%#v\n", dbManager.configs)
|
||||||
|
|
||||||
|
// Initialize all databases
|
||||||
|
for name, dbConfig := range dbManager.configs {
|
||||||
|
if err := dbManager.addDatabase(name, dbConfig); err != nil {
|
||||||
|
log.Printf("Failed to connect to database %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize read replicas
|
||||||
|
for name, replicaConfigs := range dbManager.readConfigs {
|
||||||
|
for i, replicaConfig := range replicaConfigs {
|
||||||
|
if err := dbManager.addReadReplica(name, i, replicaConfig); err != nil {
|
||||||
|
log.Printf("Failed to connect to read replica %s[%d]: %v", name, i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dbManager
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) addMongoDB(name string, config config.DatabaseConfig) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Build MongoDB URI with authentication and TLS options
|
||||||
|
uri := fmt.Sprintf("mongodb://%s:%s@%s:%d/%s",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure client options with security settings
|
||||||
|
clientOptions := options.Client().ApplyURI(uri)
|
||||||
|
|
||||||
|
// Set TLS configuration if needed
|
||||||
|
if config.RequireSSL {
|
||||||
|
clientOptions.SetTLSConfig(&tls.Config{
|
||||||
|
InsecureSkipVerify: config.SSLMode == "require",
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection timeout
|
||||||
|
clientOptions.SetConnectTimeout(config.ConnectTimeout)
|
||||||
|
clientOptions.SetServerSelectionTimeout(config.Timeout)
|
||||||
|
|
||||||
|
client, err := mongo.Connect(ctx, clientOptions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to MongoDB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping to verify connection
|
||||||
|
if err := client.Ping(ctx, nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping MongoDB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mongoClients[name] = client
|
||||||
|
log.Printf("Successfully connected to MongoDB: %s", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) openMySQLConnection(config config.DatabaseConfig) (*sql.DB, error) {
|
||||||
|
// Build connection string with security parameters
|
||||||
|
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&timeout=%s&readTimeout=%s&writeTimeout=%s",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database,
|
||||||
|
config.Timeout,
|
||||||
|
config.ReadTimeout,
|
||||||
|
config.WriteTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add SSL configuration if required
|
||||||
|
if config.RequireSSL {
|
||||||
|
connStr += "&tls=true"
|
||||||
|
if config.SSLRootCert != "" {
|
||||||
|
connStr += "&ssl-ca=" + config.SSLRootCert
|
||||||
|
}
|
||||||
|
if config.SSLCert != "" {
|
||||||
|
connStr += "&ssl-cert=" + config.SSLCert
|
||||||
|
}
|
||||||
|
if config.SSLKey != "" {
|
||||||
|
connStr += "&ssl-key=" + config.SSLKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open connection
|
||||||
|
db, err := sql.Open("mysql", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open MySQL connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) openPostgresConnection(config config.DatabaseConfig) (*sql.DB, error) {
|
||||||
|
// Build connection string with security parameters
|
||||||
|
// Convert timeout durations to seconds for pgx
|
||||||
|
connectTimeoutSec := int(config.ConnectTimeout.Seconds())
|
||||||
|
statementTimeoutSec := int(config.StatementTimeout.Seconds())
|
||||||
|
|
||||||
|
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=%d statement_timeout=%d",
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Database,
|
||||||
|
config.SSLMode,
|
||||||
|
connectTimeoutSec,
|
||||||
|
statementTimeoutSec,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.Schema != "" {
|
||||||
|
connStr += " search_path=" + config.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add SSL configuration if required
|
||||||
|
if config.RequireSSL {
|
||||||
|
connStr += " sslcert=" + config.SSLCert + " sslkey=" + config.SSLKey + " sslrootcert=" + config.SSLRootCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open connection using standard database/sql interface
|
||||||
|
db, err := sql.Open("pgx", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open PostgreSQL connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) configureSQLDB(name string, db *sql.DB, config config.DatabaseConfig) error {
|
||||||
|
// Set connection pool limits
|
||||||
|
db.SetMaxOpenConns(config.MaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(config.MaxIdleConns)
|
||||||
|
db.SetConnMaxLifetime(config.ConnMaxLifetime)
|
||||||
|
db.SetConnMaxIdleTime(config.MaxIdleTime)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sqlDatabases[name] = db
|
||||||
|
|
||||||
|
// PERUBAHAN: Tambahkan pembuatan sqlx.DB dari sql.DB yang sudah ada
|
||||||
|
dbType := DatabaseType(config.Type)
|
||||||
|
var driverName string
|
||||||
|
|
||||||
|
switch dbType {
|
||||||
|
case Postgres:
|
||||||
|
driverName = "pgx"
|
||||||
|
case MySQL:
|
||||||
|
driverName = "mysql"
|
||||||
|
case SQLServer:
|
||||||
|
driverName = "sqlserver"
|
||||||
|
case SQLite:
|
||||||
|
driverName = "sqlite3"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database type for sqlx: %s", config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat sqlx.DB dari sql.DB yang sudah ada
|
||||||
|
sqlxDB := sqlx.NewDb(db, driverName)
|
||||||
|
s.sqlxDatabases[name] = sqlxDB
|
||||||
|
|
||||||
|
log.Printf("Successfully connected to SQL database: %s", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health checks the health of all database connections by pinging each database.
|
||||||
|
func (s *service) Health() map[string]map[string]string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
// Check SQL databases
|
||||||
|
for name, db := range s.sqlDatabases {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
err := db.PingContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = fmt.Sprintf("db down: %v", err)
|
||||||
|
stats["type"] = "sql"
|
||||||
|
stats["role"] = "primary"
|
||||||
|
result[name] = stats
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["status"] = "up"
|
||||||
|
stats["message"] = "It's healthy"
|
||||||
|
stats["type"] = "sql"
|
||||||
|
stats["role"] = "primary"
|
||||||
|
|
||||||
|
dbStats := db.Stats()
|
||||||
|
stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections)
|
||||||
|
stats["in_use"] = strconv.Itoa(dbStats.InUse)
|
||||||
|
stats["idle"] = strconv.Itoa(dbStats.Idle)
|
||||||
|
stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10)
|
||||||
|
stats["wait_duration"] = dbStats.WaitDuration.String()
|
||||||
|
stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10)
|
||||||
|
stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10)
|
||||||
|
|
||||||
|
if dbStats.OpenConnections > 40 {
|
||||||
|
stats["message"] = "The database is experiencing heavy load."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.WaitCount > 1000 {
|
||||||
|
stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 {
|
||||||
|
stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings."
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 {
|
||||||
|
stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern."
|
||||||
|
}
|
||||||
|
|
||||||
|
result[name] = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check read replicas
|
||||||
|
for name, replicas := range s.readReplicas {
|
||||||
|
for i, db := range replicas {
|
||||||
|
if db == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
replicaName := fmt.Sprintf("%s_replica_%d", name, i)
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
err := db.PingContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = fmt.Sprintf("read replica down: %v", err)
|
||||||
|
stats["type"] = "sql"
|
||||||
|
stats["role"] = "replica"
|
||||||
|
result[replicaName] = stats
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["status"] = "up"
|
||||||
|
stats["message"] = "Read replica healthy"
|
||||||
|
stats["type"] = "sql"
|
||||||
|
stats["role"] = "replica"
|
||||||
|
|
||||||
|
dbStats := db.Stats()
|
||||||
|
stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections)
|
||||||
|
stats["in_use"] = strconv.Itoa(dbStats.InUse)
|
||||||
|
stats["idle"] = strconv.Itoa(dbStats.Idle)
|
||||||
|
|
||||||
|
result[replicaName] = stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check MongoDB connections
|
||||||
|
for name, client := range s.mongoClients {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats := make(map[string]string)
|
||||||
|
|
||||||
|
err := client.Ping(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
stats["status"] = "down"
|
||||||
|
stats["error"] = fmt.Sprintf("mongodb down: %v", err)
|
||||||
|
stats["type"] = "mongodb"
|
||||||
|
result[name] = stats
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["status"] = "up"
|
||||||
|
stats["message"] = "It's healthy"
|
||||||
|
stats["type"] = "mongodb"
|
||||||
|
|
||||||
|
result[name] = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB returns a specific SQL database connection by name
|
||||||
|
func (s *service) GetDB(name string) (*sql.DB, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
db, exists := s.sqlDatabases[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("database %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERUBAHAN: Tambahkan metode GetSQLXDB
|
||||||
|
// GetSQLXDB returns a specific SQLX database connection by name
|
||||||
|
func (s *service) GetSQLXDB(name string) (*sqlx.DB, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
db, exists := s.sqlxDatabases[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("database %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReadDB returns a read replica connection using round-robin load balancing
|
||||||
|
func (s *service) GetReadDB(name string) (*sql.DB, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
replicas, exists := s.readReplicas[name]
|
||||||
|
if !exists || len(replicas) == 0 {
|
||||||
|
// Fallback to primary if no replicas available
|
||||||
|
return s.GetDB(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-robin load balancing
|
||||||
|
s.readBalancer[name] = (s.readBalancer[name] + 1) % len(replicas)
|
||||||
|
selected := replicas[s.readBalancer[name]]
|
||||||
|
|
||||||
|
if selected == nil {
|
||||||
|
// Fallback to primary if replica is nil
|
||||||
|
return s.GetDB(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMongoClient returns a specific MongoDB client by name
|
||||||
|
func (s *service) GetMongoClient(name string) (*mongo.Client, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
client, exists := s.mongoClients[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("MongoDB client %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDBs returns list of available database names
|
||||||
|
func (s *service) ListDBs() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
names := make([]string, 0, len(s.sqlDatabases)+len(s.mongoClients))
|
||||||
|
|
||||||
|
for name := range s.sqlDatabases {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range s.mongoClients {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDBType returns the type of a specific database
|
||||||
|
func (s *service) GetDBType(name string) (DatabaseType, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
config, exists := s.configs[name]
|
||||||
|
if !exists {
|
||||||
|
return "", fmt.Errorf("database %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DatabaseType(config.Type), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all database connections
|
||||||
|
func (s *service) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// Close listeners first
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, db := range s.sqlDatabases {
|
||||||
|
if err := db.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close database %s: %w", name, err))
|
||||||
|
} else {
|
||||||
|
log.Printf("Disconnected from SQL database: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, replicas := range s.readReplicas {
|
||||||
|
for i, db := range replicas {
|
||||||
|
if db != nil {
|
||||||
|
if err := db.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to close read replica %s[%d]: %w", name, i, err))
|
||||||
|
} else {
|
||||||
|
log.Printf("Disconnected from read replica: %s[%d]", name, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, client := range s.mongoClients {
|
||||||
|
if err := client.Disconnect(context.Background()); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to disconnect MongoDB client %s: %w", name, err))
|
||||||
|
} else {
|
||||||
|
log.Printf("Disconnected from MongoDB: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sqlDatabases = make(map[string]*sql.DB)
|
||||||
|
s.sqlxDatabases = make(map[string]*sqlx.DB) // Reset map sqlx
|
||||||
|
s.mongoClients = make(map[string]*mongo.Client)
|
||||||
|
s.readReplicas = make(map[string][]*sql.DB)
|
||||||
|
s.configs = make(map[string]config.DatabaseConfig)
|
||||||
|
s.readConfigs = make(map[string][]config.DatabaseConfig)
|
||||||
|
s.listeners = make(map[string]*pq.Listener)
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("errors closing databases: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryDB returns primary database connection
|
||||||
|
func (s *service) GetPrimaryDB(name string) (*sql.DB, error) {
|
||||||
|
return s.GetDB(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteQuery executes a query with parameters and returns rows
|
||||||
|
func (s *service) ExecuteQuery(ctx context.Context, dbName string, query string, args ...interface{}) (*sql.Rows, error) {
|
||||||
|
db, err := s.GetDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get database %s: %w", dbName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameterized queries to prevent SQL injection
|
||||||
|
rows, err := db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteQueryRow executes a query with parameters and returns a single row
|
||||||
|
func (s *service) ExecuteQueryRow(ctx context.Context, dbName string, query string, args ...interface{}) *sql.Row {
|
||||||
|
db, err := s.GetDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
// Return an empty row with error
|
||||||
|
row := &sql.Row{}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameterized queries to prevent SQL injection
|
||||||
|
return db.QueryRowContext(ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes a query with parameters and returns the result
|
||||||
|
func (s *service) Exec(ctx context.Context, dbName string, query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
db, err := s.GetDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get database %s: %w", dbName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameterized queries to prevent SQL injection
|
||||||
|
result, err := db.ExecContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenForChanges implements PostgreSQL LISTEN/NOTIFY for real-time updates
|
||||||
|
func (s *service) ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(string, string)) error {
|
||||||
|
s.mu.RLock()
|
||||||
|
config, exists := s.configs[dbName]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("database %s not found", dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only support PostgreSQL for LISTEN/NOTIFY
|
||||||
|
if DatabaseType(config.Type) != Postgres {
|
||||||
|
return fmt.Errorf("LISTEN/NOTIFY only supported for PostgreSQL databases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create connection string for listener
|
||||||
|
// Convert timeout to seconds for pq
|
||||||
|
connectTimeoutSec := int(config.ConnectTimeout.Seconds())
|
||||||
|
|
||||||
|
connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s&connect_timeout=%d",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database,
|
||||||
|
config.SSLMode,
|
||||||
|
connectTimeoutSec,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create listener
|
||||||
|
listener := pq.NewListener(
|
||||||
|
connStr,
|
||||||
|
10*time.Second,
|
||||||
|
time.Minute,
|
||||||
|
func(ev pq.ListenerEventType, err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Database listener (%s) error: %v", dbName, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store listener for cleanup
|
||||||
|
s.listenersMu.Lock()
|
||||||
|
s.listeners[dbName] = listener
|
||||||
|
s.listenersMu.Unlock()
|
||||||
|
|
||||||
|
// Listen to specified channels
|
||||||
|
for _, channel := range channels {
|
||||||
|
err := listener.Listen(channel)
|
||||||
|
if err != nil {
|
||||||
|
listener.Close()
|
||||||
|
return fmt.Errorf("failed to listen to channel %s: %w", channel, err)
|
||||||
|
}
|
||||||
|
log.Printf("Listening to database channel: %s on %s", channel, dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening loop
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
listener.Close()
|
||||||
|
s.listenersMu.Lock()
|
||||||
|
delete(s.listeners, dbName)
|
||||||
|
s.listenersMu.Unlock()
|
||||||
|
log.Printf("Database listener for %s stopped", dbName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case n := <-listener.Notify:
|
||||||
|
if n != nil {
|
||||||
|
callback(n.Channel, n.Extra)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(90 * time.Second):
|
||||||
|
// Send ping to keep connection alive
|
||||||
|
go func() {
|
||||||
|
if err := listener.Ping(); err != nil {
|
||||||
|
log.Printf("Listener ping failed for %s: %v", dbName, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyChange sends a notification to a PostgreSQL channel
|
||||||
|
func (s *service) NotifyChange(dbName, channel, payload string) error {
|
||||||
|
db, err := s.GetDB(dbName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get database %s: %w", dbName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's PostgreSQL
|
||||||
|
s.mu.RLock()
|
||||||
|
config, exists := s.configs[dbName]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("database %s configuration not found", dbName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if DatabaseType(config.Type) != Postgres {
|
||||||
|
return fmt.Errorf("NOTIFY only supported for PostgreSQL databases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute NOTIFY with parameterized query to prevent SQL injection
|
||||||
|
query := "SELECT pg_notify($1, $2)"
|
||||||
|
_, err = db.Exec(query, channel, payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Sent notification to channel %s on %s: %s", channel, dbName, payload)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) openSQLiteConnection(config config.DatabaseConfig) (*sql.DB, error) {
|
||||||
|
// Open connection
|
||||||
|
db, err := sql.Open("sqlite3", config.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open SQLite connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable foreign key constraints and WAL mode for better security and performance
|
||||||
|
_, err = db.Exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to configure SQLite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) openSQLServerConnection(config config.DatabaseConfig) (*sql.DB, error) {
|
||||||
|
// Build connection string with security parameters
|
||||||
|
// Convert timeout to seconds for SQL Server
|
||||||
|
connectTimeoutSec := int(config.ConnectTimeout.Seconds())
|
||||||
|
|
||||||
|
connStr := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s&connection timeout=%d",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database,
|
||||||
|
connectTimeoutSec,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add SSL configuration if required
|
||||||
|
if config.RequireSSL {
|
||||||
|
connStr += "&encrypt=true"
|
||||||
|
if config.SSLRootCert != "" {
|
||||||
|
connStr += "&trustServerCertificate=false"
|
||||||
|
} else {
|
||||||
|
connStr += "&trustServerCertificate=true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open connection
|
||||||
|
db, err := sql.Open("sqlserver", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open SQL Server connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package kategori
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KategoriHandler struct {
|
||||||
|
repo IKategoriRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKategoriHandler(repo IKategoriRepository) KategoriHandler {
|
||||||
|
return KategoriHandler{repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h KategoriHandler) ListKategoriOperasi(c *gin.Context) {
|
||||||
|
list, err := h.repo.SearchableListKategori(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, list)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package kategori
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KategoriOperasiModel struct {
|
||||||
|
ID int `json:"id" db:"id"`
|
||||||
|
Status string `json:"status" db:"status" validate:"oneof=draft published"`
|
||||||
|
Kategori string `json:"kategori" db:"kategori"`
|
||||||
|
DateCreated time.Time `json:"date_created" db:"date_created"`
|
||||||
|
DateUpdated sql.NullTime `json:"date_updated" db:"date_updated"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package kategori
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
queryUtils "antrian-operasi/internal/utils/query"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DB_NAME = "db_antrian"
|
||||||
|
|
||||||
|
type IKategoriRepository interface {
|
||||||
|
SearchableListKategori(c *gin.Context) ([]KategoriOperasiModel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type kategoriRepo struct {
|
||||||
|
queryBuilder *queryUtils.QueryBuilder
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(dbService database.Service) IKategoriRepository {
|
||||||
|
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
|
||||||
|
SetAllowedColumns([]string{
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"date_created",
|
||||||
|
"date_updated",
|
||||||
|
"Kategori",
|
||||||
|
})
|
||||||
|
queryBuilder.SetSecurityOptions(false, 100)
|
||||||
|
|
||||||
|
return kategoriRepo{
|
||||||
|
queryBuilder: queryBuilder,
|
||||||
|
db: dbService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r kategoriRepo) SearchableListKategori(c *gin.Context) ([]KategoriOperasiModel, error) {
|
||||||
|
var result []KategoriOperasiModel
|
||||||
|
search := c.Query("search")
|
||||||
|
|
||||||
|
query := queryUtils.DynamicQuery{
|
||||||
|
From: "daftar_kategori_operasi",
|
||||||
|
Fields: []queryUtils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "status"},
|
||||||
|
{Expression: "date_created"},
|
||||||
|
{Expression: "date_updated"},
|
||||||
|
{Expression: "Kategori", Alias: "kategori"},
|
||||||
|
},
|
||||||
|
Sort: []queryUtils.SortField{
|
||||||
|
{Column: "date_created", Order: "DESC"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if search != "" {
|
||||||
|
searchFilters := []queryUtils.DynamicFilter{
|
||||||
|
{Column: "Kategori", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
|
||||||
|
}
|
||||||
|
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"})
|
||||||
|
}
|
||||||
|
|
||||||
|
dbconn, err := r.db.GetSQLXDB(DB_NAME)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to connect db %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.queryBuilder.ExecuteQuery(
|
||||||
|
c, dbconn, query, &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to execute query %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package kategori
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(r *gin.RouterGroup, dbService database.Service) {
|
||||||
|
ketegoriRepo := NewRepository(dbService)
|
||||||
|
kategoriHandler := NewKategoriHandler(ketegoriRepo)
|
||||||
|
|
||||||
|
kategori := r.Group("/kategori")
|
||||||
|
{
|
||||||
|
kategori.GET("", kategoriHandler.ListKategoriOperasi)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package spesialis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpesialisHandler struct {
|
||||||
|
repo ISpesialisRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpesialisHandler(repo ISpesialisRepository) SpesialisHandler {
|
||||||
|
return SpesialisHandler{repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h SpesialisHandler) ListSpesialis(c *gin.Context) {
|
||||||
|
list, err := h.repo.SearchableListSpesialis(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h SpesialisHandler) ListSubSpesialis(c *gin.Context) {
|
||||||
|
list, err := h.repo.SearchableListSubSpesialis(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, list)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package spesialis
|
||||||
|
|
||||||
|
type SpesialisModel struct {
|
||||||
|
ID int `json:"id" db:"id"`
|
||||||
|
Kode string `json:"kode" db:"Kode"`
|
||||||
|
Spesialis string `json:"spesialis" db:"Spesialis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubSpesialisModel struct {
|
||||||
|
ID int `json:"id" db:"id"`
|
||||||
|
KodeSpesialis string `json:"kode_spesialis" db:"kode_spesialis"`
|
||||||
|
Spesialis string `json:"spesialis" db:"spesialis"`
|
||||||
|
KodeSubSpesialis string `json:"kode" db:"Kode"`
|
||||||
|
SubSpesialis string `json:"sub_spesialis" db:"Subspesialis"`
|
||||||
|
IDSpesialis int `json:"id_spesialis" db:"id_spesialis"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package spesialis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
queryUtils "antrian-operasi/internal/utils/query"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DB_NAME = "db_antrian"
|
||||||
|
|
||||||
|
type ISpesialisRepository interface {
|
||||||
|
SearchableListSpesialis(c *gin.Context) ([]SpesialisModel, error)
|
||||||
|
SearchableListSubSpesialis(c *gin.Context) ([]SubSpesialisModel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type spesialisRepo struct {
|
||||||
|
queryBuilder *queryUtils.QueryBuilder
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(dbService database.Service) ISpesialisRepository {
|
||||||
|
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
|
||||||
|
SetAllowedColumns([]string{
|
||||||
|
"id",
|
||||||
|
"Kode",
|
||||||
|
"Spesialis",
|
||||||
|
"Subspesialis",
|
||||||
|
"FK_daftar_spesialis_ID",
|
||||||
|
})
|
||||||
|
queryBuilder.SetSecurityOptions(false, 100)
|
||||||
|
|
||||||
|
return spesialisRepo{
|
||||||
|
queryBuilder: queryBuilder,
|
||||||
|
db: dbService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r spesialisRepo) SearchableListSpesialis(c *gin.Context) ([]SpesialisModel, error) {
|
||||||
|
var result []SpesialisModel
|
||||||
|
search := c.Query("search")
|
||||||
|
|
||||||
|
query := queryUtils.DynamicQuery{
|
||||||
|
From: "daftar_spesialis",
|
||||||
|
Fields: []queryUtils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "Kode"},
|
||||||
|
{Expression: "Spesialis"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if search != "" {
|
||||||
|
searchFilters := []queryUtils.DynamicFilter{
|
||||||
|
{Column: "Kode", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
|
||||||
|
{Column: "Spesialis", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
|
||||||
|
}
|
||||||
|
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"})
|
||||||
|
}
|
||||||
|
|
||||||
|
dbconn, err := r.db.GetSQLXDB(DB_NAME)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to connect db %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.queryBuilder.ExecuteQuery(
|
||||||
|
c, dbconn, query, &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to execute query %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r spesialisRepo) SearchableListSubSpesialis(c *gin.Context) ([]SubSpesialisModel, error) {
|
||||||
|
var result []SubSpesialisModel
|
||||||
|
search := c.Query("search")
|
||||||
|
|
||||||
|
query := queryUtils.DynamicQuery{
|
||||||
|
From: "daftar_subspesialis",
|
||||||
|
Aliases: "dss",
|
||||||
|
Fields: []queryUtils.SelectField{
|
||||||
|
{Expression: "dss.id", Alias: "id"},
|
||||||
|
{Expression: "dss.Kode", Alias: "Kode"},
|
||||||
|
{Expression: "Subspesialis", Alias: "Subspesialis"},
|
||||||
|
{Expression: "ds.Kode", Alias: "kode_spesialis"},
|
||||||
|
{Expression: "ds.Spesialis", Alias: "spesialis"},
|
||||||
|
{Expression: "ds.id", Alias: "id_spesialis"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Joins = []queryUtils.Join{
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "daftar_spesialis",
|
||||||
|
Alias: "ds",
|
||||||
|
OnConditions: queryUtils.FilterGroup{
|
||||||
|
Filters: []queryUtils.DynamicFilter{
|
||||||
|
{
|
||||||
|
Column: "FK_daftar_spesialis_ID", Operator: queryUtils.OpEqual, Value: "ds.id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if search != "" {
|
||||||
|
searchFilters := []queryUtils.DynamicFilter{
|
||||||
|
{Column: "dss.Kode", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
|
||||||
|
{Column: "dss.Subspesialis", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
|
||||||
|
}
|
||||||
|
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"})
|
||||||
|
}
|
||||||
|
|
||||||
|
dbconn, err := r.db.GetSQLXDB(DB_NAME)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to connect db %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.queryBuilder.ExecuteQuery(
|
||||||
|
c, dbconn, query, &result)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to execute query %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package spesialis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(r *gin.RouterGroup, dbService database.Service) {
|
||||||
|
spesialisRepo := NewRepository(dbService)
|
||||||
|
spesialisHandler := NewSpesialisHandler(spesialisRepo)
|
||||||
|
|
||||||
|
r.GET("/spesialis", spesialisHandler.ListSpesialis)
|
||||||
|
r.GET("/sub-spesialis", spesialisHandler.ListSubSpesialis)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
"antrian-operasi/internal/domain/reference/kategori"
|
||||||
|
"antrian-operasi/internal/domain/reference/spesialis"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(cfg *config.Config, dbService database.Service) *gin.Engine {
|
||||||
|
gin.SetMode(cfg.Server.Mode)
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
router.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "pong",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// router.GET("databases", func(c *gin.Context) {
|
||||||
|
// c.JSON(200, gin.H{
|
||||||
|
// "databases": dbService.ListDBs(),
|
||||||
|
// "health": dbService.Health(),
|
||||||
|
// "timestamp": time.Now().Unix(),
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
api := router.Group("/api")
|
||||||
|
{
|
||||||
|
kategori.RegisterRoutes(api, dbService)
|
||||||
|
spesialis.RegisterRoutes(api, dbService)
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antrian-operasi/internal/database"
|
||||||
|
"antrian-operasi/internal/routes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"antrian-operasi/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dbService database.Service
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
port int
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer() *http.Server {
|
||||||
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
|
if dbService == nil {
|
||||||
|
dbService = database.New(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
NewServer := &Server{
|
||||||
|
port: cfg.Server.Port,
|
||||||
|
db: dbService,
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", NewServer.port),
|
||||||
|
Handler: routes.RegisterRoutes(cfg, dbService),
|
||||||
|
IdleTimeout: time.Minute,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterOperator represents supported filter operators
|
||||||
|
type FilterOperator string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpEqual FilterOperator = "_eq"
|
||||||
|
OpNotEqual FilterOperator = "_neq"
|
||||||
|
OpLike FilterOperator = "_like"
|
||||||
|
OpILike FilterOperator = "_ilike"
|
||||||
|
OpIn FilterOperator = "_in"
|
||||||
|
OpNotIn FilterOperator = "_nin"
|
||||||
|
OpGreaterThan FilterOperator = "_gt"
|
||||||
|
OpGreaterThanEqual FilterOperator = "_gte"
|
||||||
|
OpLessThan FilterOperator = "_lt"
|
||||||
|
OpLessThanEqual FilterOperator = "_lte"
|
||||||
|
OpBetween FilterOperator = "_between"
|
||||||
|
OpNotBetween FilterOperator = "_nbetween"
|
||||||
|
OpNull FilterOperator = "_null"
|
||||||
|
OpNotNull FilterOperator = "_nnull"
|
||||||
|
OpContains FilterOperator = "_contains"
|
||||||
|
OpNotContains FilterOperator = "_ncontains"
|
||||||
|
OpStartsWith FilterOperator = "_starts_with"
|
||||||
|
OpEndsWith FilterOperator = "_ends_with"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DynamicFilter represents a single filter condition
|
||||||
|
type DynamicFilter struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Operator FilterOperator `json:"operator"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
LogicOp string `json:"logic_op,omitempty"` // AND, OR
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterGroup represents a group of filters
|
||||||
|
type FilterGroup struct {
|
||||||
|
Filters []DynamicFilter `json:"filters"`
|
||||||
|
LogicOp string `json:"logic_op"` // AND, OR
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicQuery represents the complete query structure
|
||||||
|
type DynamicQuery struct {
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
Filters []FilterGroup `json:"filters,omitempty"`
|
||||||
|
Sort []SortField `json:"sort,omitempty"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
GroupBy []string `json:"group_by,omitempty"`
|
||||||
|
Having []FilterGroup `json:"having,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortField represents sorting configuration
|
||||||
|
type SortField struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Order string `json:"order"` // ASC, DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryBuilder builds SQL queries from dynamic filters
|
||||||
|
type QueryBuilder struct {
|
||||||
|
tableName string
|
||||||
|
columnMapping map[string]string // Maps API field names to DB column names
|
||||||
|
allowedColumns map[string]bool // Security: only allow specified columns
|
||||||
|
paramCounter int
|
||||||
|
mu *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryBuilder creates a new query builder instance
|
||||||
|
func NewQueryBuilder(tableName string) *QueryBuilder {
|
||||||
|
return &QueryBuilder{
|
||||||
|
tableName: tableName,
|
||||||
|
columnMapping: make(map[string]string),
|
||||||
|
allowedColumns: make(map[string]bool),
|
||||||
|
paramCounter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColumnMapping sets the mapping between API field names and database column names
|
||||||
|
func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilder {
|
||||||
|
qb.columnMapping = mapping
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAllowedColumns sets the list of allowed columns for security
|
||||||
|
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
|
||||||
|
qb.allowedColumns = make(map[string]bool)
|
||||||
|
for _, col := range columns {
|
||||||
|
qb.allowedColumns[col] = true
|
||||||
|
}
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildQuery builds the complete SQL query
|
||||||
|
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
|
qb.paramCounter = 0
|
||||||
|
|
||||||
|
// Build SELECT clause
|
||||||
|
selectClause := qb.buildSelectClause(query.Fields)
|
||||||
|
|
||||||
|
// Build FROM clause
|
||||||
|
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ORDER BY clause
|
||||||
|
orderClause := qb.buildOrderClause(query.Sort)
|
||||||
|
|
||||||
|
// Build GROUP BY clause
|
||||||
|
groupClause := qb.buildGroupByClause(query.GroupBy)
|
||||||
|
|
||||||
|
// Build HAVING clause
|
||||||
|
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
sqlParts := []string{selectClause, fromClause}
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupClause != "" {
|
||||||
|
sqlParts = append(sqlParts, groupClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
||||||
|
args = append(args, havingArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if orderClause != "" {
|
||||||
|
sqlParts = append(sqlParts, orderClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pagination
|
||||||
|
if query.Limit > 0 {
|
||||||
|
qb.paramCounter++
|
||||||
|
sqlParts = append(sqlParts, fmt.Sprintf("LIMIT $%d", qb.paramCounter))
|
||||||
|
args = append(args, query.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Offset > 0 {
|
||||||
|
qb.paramCounter++
|
||||||
|
sqlParts = append(sqlParts, fmt.Sprintf("OFFSET $%d", qb.paramCounter))
|
||||||
|
args = append(args, query.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSelectClause builds the SELECT part of the query
|
||||||
|
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
|
||||||
|
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
|
||||||
|
return "SELECT *"
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedFields []string
|
||||||
|
for _, field := range fields {
|
||||||
|
if field == "*.*" || field == "*" {
|
||||||
|
selectedFields = append(selectedFields, "*")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an expression (contains spaces, parentheses, etc.)
|
||||||
|
if strings.Contains(field, " ") || strings.Contains(field, "(") || strings.Contains(field, ")") {
|
||||||
|
// Expression, add as is
|
||||||
|
selectedFields = append(selectedFields, field)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check: only allow specified columns (check original field name)
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[field] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map field name if mapping exists
|
||||||
|
if mappedCol, exists := qb.columnMapping[field]; exists {
|
||||||
|
field = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, field))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(selectedFields) == 0 {
|
||||||
|
return "SELECT *"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SELECT " + strings.Join(selectedFields, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWhereClause builds the WHERE part of the query
|
||||||
|
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(filterGroups) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var conditions []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
for i, group := range filterGroups {
|
||||||
|
groupCondition, groupArgs, err := qb.buildFilterGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupCondition != "" {
|
||||||
|
if i > 0 {
|
||||||
|
logicOp := "AND"
|
||||||
|
if group.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(group.LogicOp)
|
||||||
|
}
|
||||||
|
conditions = append(conditions, logicOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, groupCondition)
|
||||||
|
args = append(args, groupArgs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(conditions, " "), args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFilterGroup builds conditions for a filter group
|
||||||
|
func (qb *QueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(group.Filters) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var conditions []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
for i, filter := range group.Filters {
|
||||||
|
condition, filterArgs, err := qb.buildFilterCondition(filter)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition != "" {
|
||||||
|
if i > 0 {
|
||||||
|
logicOp := "AND"
|
||||||
|
if filter.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(filter.LogicOp)
|
||||||
|
} else if group.LogicOp != "" {
|
||||||
|
logicOp = strings.ToUpper(group.LogicOp)
|
||||||
|
}
|
||||||
|
conditions = append(conditions, logicOp)
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, condition)
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(conditions, " "), args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFilterCondition builds a single filter condition
|
||||||
|
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []interface{}, error) {
|
||||||
|
// Security check (check original field name)
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[filter.Column] {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map column name if mapping exists
|
||||||
|
column := filter.Column
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap column name in quotes for PostgreSQL
|
||||||
|
column = fmt.Sprintf(`"%s"`, column)
|
||||||
|
|
||||||
|
switch filter.Operator {
|
||||||
|
case OpEqual:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s = $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpNotEqual:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s != $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLike:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s LIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpILike:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpIn:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders []string
|
||||||
|
var args []interface{}
|
||||||
|
for _, val := range values {
|
||||||
|
qb.paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
|
||||||
|
args = append(args, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
|
||||||
|
|
||||||
|
case OpNotIn:
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders []string
|
||||||
|
var args []interface{}
|
||||||
|
for _, val := range values {
|
||||||
|
qb.paramCounter++
|
||||||
|
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
|
||||||
|
args = append(args, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s NOT IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
|
||||||
|
|
||||||
|
case OpGreaterThan:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s > $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpGreaterThanEqual:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s >= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLessThan:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s < $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpLessThanEqual:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
return fmt.Sprintf("%s <= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
|
||||||
|
|
||||||
|
case OpBetween:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("between operator requires exactly 2 values")
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
param1 := qb.paramCounter
|
||||||
|
qb.paramCounter++
|
||||||
|
param2 := qb.paramCounter
|
||||||
|
return fmt.Sprintf("%s BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
|
||||||
|
|
||||||
|
case OpNotBetween:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
values := qb.parseArrayValue(filter.Value)
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("not between operator requires exactly 2 values")
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
param1 := qb.paramCounter
|
||||||
|
qb.paramCounter++
|
||||||
|
param2 := qb.paramCounter
|
||||||
|
return fmt.Sprintf("%s NOT BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
|
||||||
|
|
||||||
|
case OpNull:
|
||||||
|
return fmt.Sprintf("%s IS NULL", column), nil, nil
|
||||||
|
|
||||||
|
case OpNotNull:
|
||||||
|
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
|
||||||
|
|
||||||
|
case OpContains:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpNotContains:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s NOT ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpStartsWith:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%v%%", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
case OpEndsWith:
|
||||||
|
if filter.Value == nil {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
qb.paramCounter++
|
||||||
|
value := fmt.Sprintf("%%%v", filter.Value)
|
||||||
|
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unsupported operator: %s", filter.Operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseArrayValue parses array values from various formats
|
||||||
|
func (qb *QueryBuilder) parseArrayValue(value interface{}) []interface{} {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already a slice
|
||||||
|
if reflect.TypeOf(value).Kind() == reflect.Slice {
|
||||||
|
v := reflect.ValueOf(value)
|
||||||
|
result := make([]interface{}, v.Len())
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
result[i] = v.Index(i).Interface()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a string, try to split by comma
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
if strings.Contains(str, ",") {
|
||||||
|
parts := strings.Split(str, ",")
|
||||||
|
result := make([]interface{}, len(parts))
|
||||||
|
for i, part := range parts {
|
||||||
|
result[i] = strings.TrimSpace(part)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return []interface{}{str}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []interface{}{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOrderClause builds the ORDER BY clause
|
||||||
|
func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
|
||||||
|
if len(sortFields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderParts []string
|
||||||
|
for _, sort := range sortFields {
|
||||||
|
column := sort.Column
|
||||||
|
|
||||||
|
// Security check (check original field name)
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
order := "ASC"
|
||||||
|
if sort.Order != "" {
|
||||||
|
order = strings.ToUpper(sort.Order)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderParts = append(orderParts, fmt.Sprintf(`"%s" %s`, column, order))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(orderParts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ORDER BY " + strings.Join(orderParts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGroupByClause builds the GROUP BY clause
|
||||||
|
func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string {
|
||||||
|
if len(groupFields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupParts []string
|
||||||
|
for _, field := range groupFields {
|
||||||
|
column := field
|
||||||
|
if mappedCol, exists := qb.columnMapping[column]; exists {
|
||||||
|
column = mappedCol
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groupParts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "GROUP BY " + strings.Join(groupParts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHavingClause builds the HAVING clause
|
||||||
|
func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup) (string, []interface{}, error) {
|
||||||
|
if len(havingGroups) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.buildWhereClause(havingGroups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCountQuery builds a count query
|
||||||
|
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
|
||||||
|
qb.paramCounter = 0
|
||||||
|
|
||||||
|
// Build FROM clause
|
||||||
|
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build GROUP BY clause
|
||||||
|
groupClause := qb.buildGroupByClause(query.GroupBy)
|
||||||
|
|
||||||
|
// Build HAVING clause
|
||||||
|
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine parts
|
||||||
|
sqlParts := []string{"SELECT COUNT(*)", fromClause}
|
||||||
|
args := []interface{}{}
|
||||||
|
|
||||||
|
if whereClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "WHERE "+whereClause)
|
||||||
|
args = append(args, whereArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupClause != "" {
|
||||||
|
sqlParts = append(sqlParts, groupClause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
sqlParts = append(sqlParts, "HAVING "+havingClause)
|
||||||
|
args = append(args, havingArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := strings.Join(sqlParts, " ")
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryParser parses HTTP query parameters into DynamicQuery
|
||||||
|
type QueryParser struct {
|
||||||
|
defaultLimit int
|
||||||
|
maxLimit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueryParser creates a new query parser
|
||||||
|
func NewQueryParser() *QueryParser {
|
||||||
|
return &QueryParser{
|
||||||
|
defaultLimit: 10,
|
||||||
|
maxLimit: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLimits sets default and maximum limits
|
||||||
|
func (qp *QueryParser) SetLimits(defaultLimit, maxLimit int) *QueryParser {
|
||||||
|
qp.defaultLimit = defaultLimit
|
||||||
|
qp.maxLimit = maxLimit
|
||||||
|
return qp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseQuery parses URL query parameters into DynamicQuery
|
||||||
|
func (qp *QueryParser) ParseQuery(values url.Values) (DynamicQuery, error) {
|
||||||
|
query := DynamicQuery{
|
||||||
|
Limit: qp.defaultLimit,
|
||||||
|
Offset: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse fields
|
||||||
|
if fields := values.Get("fields"); fields != "" {
|
||||||
|
if fields == "*.*" || fields == "*" {
|
||||||
|
query.Fields = []string{"*"}
|
||||||
|
} else {
|
||||||
|
query.Fields = strings.Split(fields, ",")
|
||||||
|
for i, field := range query.Fields {
|
||||||
|
query.Fields[i] = strings.TrimSpace(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pagination
|
||||||
|
if limit := values.Get("limit"); limit != "" {
|
||||||
|
if l, err := strconv.Atoi(limit); err == nil {
|
||||||
|
if l > 0 && l <= qp.maxLimit {
|
||||||
|
query.Limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset := values.Get("offset"); offset != "" {
|
||||||
|
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
|
||||||
|
query.Offset = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filters
|
||||||
|
filters, err := qp.parseFilters(values)
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
query.Filters = filters
|
||||||
|
|
||||||
|
// Parse sorting
|
||||||
|
sorts, err := qp.parseSorting(values)
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
query.Sort = sorts
|
||||||
|
|
||||||
|
// Parse group by
|
||||||
|
if groupBy := values.Get("group"); groupBy != "" {
|
||||||
|
query.GroupBy = strings.Split(groupBy, ",")
|
||||||
|
for i, field := range query.GroupBy {
|
||||||
|
query.GroupBy[i] = strings.TrimSpace(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilters parses filter parameters
|
||||||
|
// Supports format: filter[column][operator]=value
|
||||||
|
func (qp *QueryParser) parseFilters(values url.Values) ([]FilterGroup, error) {
|
||||||
|
filterMap := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
// Group filters by column
|
||||||
|
for key, vals := range values {
|
||||||
|
if strings.HasPrefix(key, "filter[") && strings.HasSuffix(key, "]") {
|
||||||
|
// Parse filter[column][operator] format
|
||||||
|
parts := strings.Split(key[7:len(key)-1], "][")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
column := parts[0]
|
||||||
|
operator := parts[1]
|
||||||
|
|
||||||
|
if filterMap[column] == nil {
|
||||||
|
filterMap[column] = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vals) > 0 {
|
||||||
|
filterMap[column][operator] = vals[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filterMap) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to FilterGroup
|
||||||
|
var filters []DynamicFilter
|
||||||
|
|
||||||
|
for column, operators := range filterMap {
|
||||||
|
for opStr, value := range operators {
|
||||||
|
operator := FilterOperator(opStr)
|
||||||
|
|
||||||
|
// Parse value based on operator
|
||||||
|
var parsedValue interface{}
|
||||||
|
switch operator {
|
||||||
|
case OpIn, OpNotIn:
|
||||||
|
if value != "" {
|
||||||
|
parsedValue = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
case OpBetween, OpNotBetween:
|
||||||
|
if value != "" {
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
parsedValue = []interface{}{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case OpNull, OpNotNull:
|
||||||
|
parsedValue = nil
|
||||||
|
default:
|
||||||
|
parsedValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = append(filters, DynamicFilter{
|
||||||
|
Column: column,
|
||||||
|
Operator: operator,
|
||||||
|
Value: parsedValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filters) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []FilterGroup{{
|
||||||
|
Filters: filters,
|
||||||
|
LogicOp: "AND",
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSorting parses sort parameters
|
||||||
|
// Supports format: sort=column1,-column2 (- for DESC)
|
||||||
|
func (qp *QueryParser) parseSorting(values url.Values) ([]SortField, error) {
|
||||||
|
sortParam := values.Get("sort")
|
||||||
|
if sortParam == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sorts []SortField
|
||||||
|
fields := strings.Split(sortParam, ",")
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
order := "ASC"
|
||||||
|
column := field
|
||||||
|
|
||||||
|
if strings.HasPrefix(field, "-") {
|
||||||
|
order = "DESC"
|
||||||
|
column = field[1:]
|
||||||
|
} else if strings.HasPrefix(field, "+") {
|
||||||
|
column = field[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
sorts = append(sorts, SortField{
|
||||||
|
Column: column,
|
||||||
|
Order: order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAdvancedFilters parses complex filter structures
|
||||||
|
// Supports nested filters and logic operators
|
||||||
|
func (qp *QueryParser) ParseAdvancedFilters(filterParam string) ([]FilterGroup, error) {
|
||||||
|
// This would be for more complex JSON-based filters
|
||||||
|
// Implementation depends on your specific needs
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse date values
|
||||||
|
func parseDate(value string) (interface{}, error) {
|
||||||
|
// Try different date formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05.000Z",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.Parse(format, value); err == nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse numeric values
|
||||||
|
func parseNumeric(value string) interface{} {
|
||||||
|
// Try integer first
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try float
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
@@ -0,0 +1,918 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/lib/pq" // PostgreSQL driver
|
||||||
|
"yourpackage/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Inisialisasi koneksi database
|
||||||
|
db, err := sqlx.Connect("postgres", "user=postgres dbname=testdb sslmode=disable")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Inisialisasi QueryBuilder
|
||||||
|
qb := utils.NewQueryBuilder(utils.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
// Contoh penggunaan
|
||||||
|
simpleQueryExample(db, qb)
|
||||||
|
complexQueryExample(db, qb)
|
||||||
|
nestedJoinExample(db, qb)
|
||||||
|
multiJoinExample(db, qb)
|
||||||
|
commonQueriesExample(db, qb)
|
||||||
|
jsonQueryExample(db, qb)
|
||||||
|
windowFunctionExample(db, qb)
|
||||||
|
cteExample(db, qb)
|
||||||
|
unionExample(db, qb)
|
||||||
|
aggregateExample(db, qb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Simple Query Example ===")
|
||||||
|
|
||||||
|
// Query sederhana dengan filter
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "users",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id", Alias: "user_id"},
|
||||||
|
{Expression: "name", Alias: "user_name"},
|
||||||
|
{Expression: "email"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
{Column: "created_at", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(0, -1, 0)},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "name", Order: "ASC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing simple query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d users\n", len(results))
|
||||||
|
for _, user := range results {
|
||||||
|
fmt.Printf("User: %+v\n", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func complexQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Complex Query Example ===")
|
||||||
|
|
||||||
|
// Query dengan nested filter dan berbagai operator
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "orders",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id", Alias: "order_id"},
|
||||||
|
{Expression: "customer_id"},
|
||||||
|
{Expression: "total_amount"},
|
||||||
|
{Expression: "order_date"},
|
||||||
|
{Expression: "status"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpIn, Value: []string{"completed", "processing"}},
|
||||||
|
{Column: "total_amount", Operator: utils.OpGreaterThan, Value: 1000},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "order_date", Operator: utils.OpBetween, Value: []interface{}{time.Now().AddDate(0, -3, 0), time.Now()}},
|
||||||
|
{Column: "customer_id", Operator: utils.OpNotIn, Value: []int{1, 2, 3}},
|
||||||
|
},
|
||||||
|
LogicOp: "OR",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "order_date", Order: "DESC"},
|
||||||
|
{Column: "total_amount", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 20,
|
||||||
|
Offset: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing complex query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d orders\n", len(results))
|
||||||
|
for _, order := range results {
|
||||||
|
fmt.Printf("Order: %+v\n", order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedJoinExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Nested Join Example ===")
|
||||||
|
|
||||||
|
// Query dengan nested join
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "customers",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "customers.id", Alias: "customer_id"},
|
||||||
|
{Expression: "customers.name", Alias: "customer_name"},
|
||||||
|
{Expression: "orders.id", Alias: "order_id"},
|
||||||
|
{Expression: "orders.total_amount"},
|
||||||
|
{Expression: "order_items.product_id"},
|
||||||
|
{Expression: "order_items.quantity"},
|
||||||
|
{Expression: "products.name", Alias: "product_name"},
|
||||||
|
},
|
||||||
|
Joins: []utils.Join{
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "orders",
|
||||||
|
Alias: "orders",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "customers.id", Operator: utils.OpEqual, Value: "orders.customer_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "order_items",
|
||||||
|
Alias: "order_items",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "orders.id", Operator: utils.OpEqual, Value: "order_items.order_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "products",
|
||||||
|
Alias: "products",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "order_items.product_id", Operator: utils.OpEqual, Value: "products.id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "customers.status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
{Column: "orders.status", Operator: utils.OpEqual, Value: "completed"},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "customers.name", Order: "ASC"},
|
||||||
|
{Column: "orders.id", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing nested join query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d customer-order-product records\n", len(results))
|
||||||
|
for _, record := range results {
|
||||||
|
fmt.Printf("Record: %+v\n", record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func multiJoinExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Multi Join Example ===")
|
||||||
|
|
||||||
|
// Query dengan multiple join types
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "employees",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "employees.id", Alias: "employee_id"},
|
||||||
|
{Expression: "employees.name", Alias: "employee_name"},
|
||||||
|
{Expression: "departments.name", Alias: "department_name"},
|
||||||
|
{Expression: "projects.name", Alias: "project_name"},
|
||||||
|
{Expression: "tasks.title", Alias: "task_title"},
|
||||||
|
{Expression: "task_assignments.assigned_date"},
|
||||||
|
},
|
||||||
|
Joins: []utils.Join{
|
||||||
|
{
|
||||||
|
Type: "INNER",
|
||||||
|
Table: "departments",
|
||||||
|
Alias: "departments",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "employees.department_id", Operator: utils.OpEqual, Value: "departments.id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "task_assignments",
|
||||||
|
Alias: "task_assignments",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "employees.id", Operator: utils.OpEqual, Value: "task_assignments.employee_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "tasks",
|
||||||
|
Alias: "tasks",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "task_assignments.task_id", Operator: utils.OpEqual, Value: "tasks.id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "LEFT",
|
||||||
|
Table: "projects",
|
||||||
|
Alias: "projects",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "tasks.project_id", Operator: utils.OpEqual, Value: "projects.id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "employees.status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
{Column: "departments.status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "departments.name", Order: "ASC"},
|
||||||
|
{Column: "employees.name", Order: "ASC"},
|
||||||
|
{Column: "task_assignments.assigned_date", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing multi join query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d employee-task-project records\n", len(results))
|
||||||
|
for _, record := range results {
|
||||||
|
fmt.Printf("Record: %+v\n", record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonQueriesExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Common Queries Example ===")
|
||||||
|
|
||||||
|
// 1. Query dengan LIKE/ILIKE
|
||||||
|
likeQuery := utils.DynamicQuery{
|
||||||
|
From: "products",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "price"},
|
||||||
|
{Expression: "category"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "name", Operator: utils.OpILike, Value: "%laptop%"},
|
||||||
|
{Column: "category", Operator: utils.OpEqual, Value: "electronics"},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "price", Order: "ASC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var products []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, likeQuery, &products)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing LIKE query: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Found %d products matching 'laptop'\n", len(products))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Query dengan pagination
|
||||||
|
page := 2
|
||||||
|
pageSize := 20
|
||||||
|
paginationQuery := utils.DynamicQuery{
|
||||||
|
From: "orders",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "customer_id"},
|
||||||
|
{Expression: "total_amount"},
|
||||||
|
{Expression: "order_date"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "completed"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "order_date", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: pageSize,
|
||||||
|
Offset: (page - 1) * pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
var orders []map[string]interface{}
|
||||||
|
err = qb.ExecuteQuery(context.Background(), db, paginationQuery, &orders)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing pagination query: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Found %d orders on page %d\n", len(orders), page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Query dengan NULL/NOT NULL
|
||||||
|
nullQuery := utils.DynamicQuery{
|
||||||
|
From: "customers",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "email"},
|
||||||
|
{Expression: "phone"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "email", Operator: utils.OpNotNull},
|
||||||
|
{Column: "phone", Operator: utils.OpNull},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var customers []map[string]interface{}
|
||||||
|
err = qb.ExecuteQuery(context.Background(), db, nullQuery, &customers)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing NULL query: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Found %d customers with email but no phone\n", len(customers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Query dengan BETWEEN
|
||||||
|
betweenQuery := utils.DynamicQuery{
|
||||||
|
From: "transactions",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "account_id"},
|
||||||
|
{Expression: "amount"},
|
||||||
|
{Expression: "transaction_date"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "amount", Operator: utils.OpBetween, Value: []interface{}{100, 1000}},
|
||||||
|
{Column: "transaction_date", Operator: utils.OpBetween, Value: []interface{}{time.Now().AddDate(0, -1, 0), time.Now()}},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "transaction_date", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactions []map[string]interface{}
|
||||||
|
err = qb.ExecuteQuery(context.Background(), db, betweenQuery, &transactions)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing BETWEEN query: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Found %d transactions between $100 and $1000 in the last month\n", len(transactions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonQueryExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== JSON Query Example ===")
|
||||||
|
|
||||||
|
// Query dengan operasi JSON
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "products",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "price"},
|
||||||
|
{Expression: "attributes"},
|
||||||
|
},
|
||||||
|
JsonOperations: []utils.JsonOperation{
|
||||||
|
{
|
||||||
|
Type: "extract",
|
||||||
|
Column: "attributes",
|
||||||
|
Path: "$.color",
|
||||||
|
Alias: "color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "extract",
|
||||||
|
Column: "attributes",
|
||||||
|
Path: "$.size",
|
||||||
|
Alias: "size",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{
|
||||||
|
Column: "attributes",
|
||||||
|
Operator: utils.OpJsonContains,
|
||||||
|
Value: map[string]interface{}{"category": "electronics"},
|
||||||
|
Options: map[string]interface{}{"path": "$"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "name", Order: "ASC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing JSON query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d products with JSON attributes\n", len(results))
|
||||||
|
for _, product := range results {
|
||||||
|
fmt.Printf("Product: %+v\n", product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowFunctionExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Window Function Example ===")
|
||||||
|
|
||||||
|
// Query dengan window functions
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
From: "sales",
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "salesperson_id"},
|
||||||
|
{Expression: "amount"},
|
||||||
|
{Expression: "sale_date"},
|
||||||
|
},
|
||||||
|
WindowFunctions: []utils.WindowFunction{
|
||||||
|
{
|
||||||
|
Function: "ROW_NUMBER",
|
||||||
|
Over: "salesperson_id",
|
||||||
|
OrderBy: "amount DESC",
|
||||||
|
Alias: "sales_rank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "SUM",
|
||||||
|
Over: "salesperson_id",
|
||||||
|
OrderBy: "sale_date",
|
||||||
|
Frame: "ROWS UNBOUNDED PRECEDING",
|
||||||
|
Alias: "running_total",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "sale_date", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(0, -6, 0)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "salesperson_id", Order: "ASC"},
|
||||||
|
{Column: "amount", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing window function query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d sales records with window functions\n", len(results))
|
||||||
|
for _, sale := range results {
|
||||||
|
fmt.Printf("Sale: %+v\n", sale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cteExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== CTE Example ===")
|
||||||
|
|
||||||
|
// Query dengan CTE
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
CTEs: []utils.CTE{
|
||||||
|
{
|
||||||
|
Name: "monthly_sales",
|
||||||
|
Query: utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "salesperson_id"},
|
||||||
|
{Expression: "EXTRACT(MONTH FROM sale_date) AS month"},
|
||||||
|
{Expression: "SUM(amount) AS total"},
|
||||||
|
},
|
||||||
|
From: "sales",
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "sale_date", Operator: utils.OpGreaterThanEqual, Value: time.Now().AddDate(-1, 0, 0)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []string{"salesperson_id", "EXTRACT(MONTH FROM sale_date)"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "top_salespeople",
|
||||||
|
Query: utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "salesperson_id"},
|
||||||
|
{Expression: "SUM(total) AS yearly_total"},
|
||||||
|
},
|
||||||
|
From: "monthly_sales",
|
||||||
|
GroupBy: []string{"salesperson_id"},
|
||||||
|
Having: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "SUM(total)", Operator: utils.OpGreaterThan, Value: 10000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "salespeople.id"},
|
||||||
|
{Expression: "salespeople.name"},
|
||||||
|
{Expression: "top_salespeople.yearly_total"},
|
||||||
|
},
|
||||||
|
From: "salespeople",
|
||||||
|
Joins: []utils.Join{
|
||||||
|
{
|
||||||
|
Type: "INNER",
|
||||||
|
Table: "top_salespeople",
|
||||||
|
Alias: "top_salespeople",
|
||||||
|
OnConditions: utils.FilterGroup{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "salespeople.id", Operator: utils.OpEqual, Value: "top_salespeople.salesperson_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "top_salespeople.yearly_total", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing CTE query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d top salespeople\n", len(results))
|
||||||
|
for _, salesperson := range results {
|
||||||
|
fmt.Printf("Salesperson: %+v\n", salesperson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unionExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== UNION Example ===")
|
||||||
|
|
||||||
|
// Query dengan UNION
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "email"},
|
||||||
|
{Expression: "'customer' AS user_type"},
|
||||||
|
},
|
||||||
|
From: "customers",
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Unions: []utils.Union{
|
||||||
|
{
|
||||||
|
Type: "UNION ALL",
|
||||||
|
Query: utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "email"},
|
||||||
|
{Expression: "'employee' AS user_type"},
|
||||||
|
},
|
||||||
|
From: "employees",
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "name", Order: "ASC"},
|
||||||
|
},
|
||||||
|
Limit: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing UNION query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d users (customers + employees)\n", len(results))
|
||||||
|
for _, user := range results {
|
||||||
|
fmt.Printf("User: %+v\n", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregateExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Aggregate Example ===")
|
||||||
|
|
||||||
|
// Query dengan fungsi agregasi
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "category"},
|
||||||
|
{Expression: "COUNT(*) AS product_count"},
|
||||||
|
{Expression: "AVG(price) AS avg_price"},
|
||||||
|
{Expression: "MIN(price) AS min_price"},
|
||||||
|
{Expression: "MAX(price) AS max_price"},
|
||||||
|
{Expression: "SUM(stock_quantity) AS total_stock"},
|
||||||
|
},
|
||||||
|
From: "products",
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []string{"category"},
|
||||||
|
Having: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "COUNT(*)", Operator: utils.OpGreaterThan, Value: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "product_count", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing aggregate query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d product categories\n", len(results))
|
||||||
|
for _, category := range results {
|
||||||
|
fmt.Printf("Category: %+v\n", category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func crudOperationsExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== CRUD Operations Example ===")
|
||||||
|
|
||||||
|
// INSERT
|
||||||
|
insertData := utils.InsertData{
|
||||||
|
Columns: []string{"name", "email", "status", "created_at"},
|
||||||
|
Values: []interface{}{"John Doe", "[email protected]", "active", time.Now()},
|
||||||
|
JsonValues: map[string]interface{}{
|
||||||
|
"preferences": map[string]interface{}{
|
||||||
|
"theme": "dark",
|
||||||
|
"language": "en",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := qb.ExecuteInsert(context.Background(), db, "customers", insertData, "id")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing INSERT: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting inserted ID: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Inserted customer with ID: %d\n", id)
|
||||||
|
|
||||||
|
// UPDATE
|
||||||
|
updateData := utils.UpdateData{
|
||||||
|
Columns: []string{"name", "status"},
|
||||||
|
Values: []interface{}{"John Smith", "inactive"},
|
||||||
|
JsonUpdates: map[string]utils.JsonUpdate{
|
||||||
|
"preferences": {
|
||||||
|
Path: "$.theme",
|
||||||
|
Value: "light",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "id", Operator: utils.OpEqual, Value: id},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = qb.ExecuteUpdate(context.Background(), db, "customers", updateData, filters, "updated_at")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing UPDATE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting rows affected: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updated %d customer(s)\n", rowsAffected)
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
result, err = qb.ExecuteDelete(context.Background(), db, "customers", filters)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing DELETE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err = result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting rows affected: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deleted %d customer(s)\n", rowsAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func mongoExample() {
|
||||||
|
fmt.Println("\n=== MongoDB Example ===")
|
||||||
|
|
||||||
|
// Inisialisasi koneksi MongoDB
|
||||||
|
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://localhost:27017"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
defer client.Disconnect(context.Background())
|
||||||
|
|
||||||
|
db := client.Database("testdb")
|
||||||
|
collection := db.Collection("users")
|
||||||
|
|
||||||
|
// Inisialisasi MongoQueryBuilder
|
||||||
|
mqb := utils.NewMongoQueryBuilder()
|
||||||
|
|
||||||
|
// Query sederhana
|
||||||
|
query := utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "email"},
|
||||||
|
{Expression: "status"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
{Column: "age", Operator: utils.OpGreaterThan, Value: 18},
|
||||||
|
},
|
||||||
|
LogicOp: "AND",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "name", Order: "ASC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err = mqb.ExecuteFind(context.Background(), collection, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing MongoDB query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d users\n", len(results))
|
||||||
|
for _, user := range results {
|
||||||
|
fmt.Printf("User: %+v\n", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregation pipeline
|
||||||
|
aggQuery := utils.DynamicQuery{
|
||||||
|
Fields: []utils.SelectField{
|
||||||
|
{Expression: "department", Alias: "_id"},
|
||||||
|
{Expression: "COUNT(*)", Alias: "employee_count"},
|
||||||
|
{Expression: "AVG(salary)", Alias: "avg_salary"},
|
||||||
|
},
|
||||||
|
Filters: []utils.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []utils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: utils.OpEqual, Value: "active"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []string{"department"},
|
||||||
|
Sort: []utils.SortField{
|
||||||
|
{Column: "employee_count", Order: "DESC"},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggResults []map[string]interface{}
|
||||||
|
err = mqb.ExecuteAggregate(context.Background(), collection, aggQuery, &aggResults)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing MongoDB aggregation: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d departments\n", len(aggResults))
|
||||||
|
for _, dept := range aggResults {
|
||||||
|
fmt.Printf("Department: %+v\n", dept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryParserExample(db *sqlx.DB, qb *utils.QueryBuilder) {
|
||||||
|
fmt.Println("\n=== Query Parser Example ===")
|
||||||
|
|
||||||
|
// Inisialisasi QueryParser
|
||||||
|
qp := utils.NewQueryParser()
|
||||||
|
|
||||||
|
// Parse URL query parameters
|
||||||
|
values := url.Values{}
|
||||||
|
values.Add("fields", "id,name,email,status")
|
||||||
|
values.Add("filter[status][_eq]", "active")
|
||||||
|
values.Add("filter[created_at][_gte]", "2023-01-01")
|
||||||
|
values.Add("filter[age][_between]", "18,65")
|
||||||
|
values.Add("sort", "+name,-created_at")
|
||||||
|
values.Add("limit", "20")
|
||||||
|
values.Add("offset", "10")
|
||||||
|
|
||||||
|
// Parse query parameters into DynamicQuery
|
||||||
|
query, err := qp.ParseQuery(values, "users")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error parsing query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the parsed query
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err = qb.ExecuteQuery(context.Background(), db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing parsed query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found %d users using parsed query\n", len(results))
|
||||||
|
for _, user := range results {
|
||||||
|
fmt.Printf("User: %+v\n", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,943 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"api-service/internal/config"
|
||||||
|
"api-service/internal/database"
|
||||||
|
"api-service/internal/utils/query"
|
||||||
|
"api-service/internal/validation"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file provides comprehensive examples of using the query builder library
|
||||||
|
// for performing various database operations including CRUD, transactions, joins, etc.
|
||||||
|
// Each example function demonstrates how to build queries, print them, and execute them.
|
||||||
|
// =============================================================================
|
||||||
|
// DEFINISI MODEL (CONTOH)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// User adalah contoh struct untuk tabel 'users'.
|
||||||
|
type User struct {
|
||||||
|
ID int `db:"id" bson:"_id,omitempty"`
|
||||||
|
Name string `db:"name" bson:"name"`
|
||||||
|
Email string `db:"email" bson:"email"`
|
||||||
|
Status string `db:"status" bson:"status"`
|
||||||
|
CreatedAt time.Time `db:"created_at" bson:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post adalah contoh struct untuk tabel 'posts'.
|
||||||
|
type Post struct {
|
||||||
|
ID int `db:"id" bson:"_id,omitempty"`
|
||||||
|
UserID int `db:"user_id" bson:"user_id"`
|
||||||
|
Title string `db:"title" bson:"title"`
|
||||||
|
Content string `db:"content" bson:"content"`
|
||||||
|
CreatedAt time.Time `db:"created_at" bson:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee adalah contoh struct untuk tabel 'employees' dengan kolom JSON.
|
||||||
|
type Employee struct {
|
||||||
|
ID int `db:"id" bson:"_id,omitempty"`
|
||||||
|
Name string `db:"name" bson:"name"`
|
||||||
|
Department string `db:"department" bson:"department"`
|
||||||
|
Salary float64 `db:"salary" bson:"salary"`
|
||||||
|
Metadata map[string]interface{} `db:"metadata" bson:"metadata"` // Kolom JSON/JSONB
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FUNGSI UTAMA
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := setupConfig()
|
||||||
|
dbService := database.New(cfg)
|
||||||
|
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
fmt.Println(" CONTOH 1: QUERY DASAR (SELECT, INSERT, UPDATE, DELETE)")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
basicCRUDExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 2: TRANSAKSI SQL (POSTGRESQL)")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
sqlTransactionExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 3: TRANSAKSI MONGODB")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
mongoTransactionExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 4: QUERY DENGAN FILTER DAN PAGINASI")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
filterAndPaginationExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 5: QUERY DENGAN JOIN")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
joinExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 6: QUERY DENGAN CTE (COMMON TABLE EXPRESSION)")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
cteExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 7: QUERY DENGAN WINDOW FUNCTION")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
windowFunctionExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 8: VALIDASI DATA DINAMIS")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
validationExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 9: OPERASI JSON")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
jsonQueryExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 10: QUERY MONGODB (CRUD & AGGREGATION)")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
mongodbExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 11: PENGGUNAAN READ REPLICA")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
readReplicaExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 12: HEALTH CHECK DATABASE")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
healthCheckExample(dbService)
|
||||||
|
|
||||||
|
fmt.Println("\n============================================================")
|
||||||
|
fmt.Println(" CONTOH 13: PARSING QUERY DARI URL")
|
||||||
|
fmt.Println("============================================================")
|
||||||
|
urlQueryParsingExample(dbService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupConfig() *config.Config {
|
||||||
|
return &config.Config{
|
||||||
|
Databases: map[string]config.DatabaseConfig{
|
||||||
|
"main": {
|
||||||
|
Type: "postgres",
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
Username: "user",
|
||||||
|
Password: "password",
|
||||||
|
Database: "company_db",
|
||||||
|
SSLMode: "disable",
|
||||||
|
MaxOpenConns: 25,
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
ConnMaxLifetime: time.Hour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mongodb": config.DatabaseConfig{
|
||||||
|
Type: "mongodb",
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 27017,
|
||||||
|
Database: "company_db",
|
||||||
|
Username: "user",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 1: QUERY DASAR (CRUD)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// basicCRUDExample demonstrates basic Create, Read, Update, Delete operations using the query builder.
|
||||||
|
// It shows how to build SQL queries, print them, and execute them while displaying results.
|
||||||
|
// Expected output: Prints INSERT SQL and result (new ID), SELECT SQL and user data, UPDATE SQL and affected rows, DELETE SQL and affected rows.
|
||||||
|
// Example raw queries:
|
||||||
|
// INSERT: INSERT INTO users (name, email, status) VALUES ($1, $2, $3) RETURNING id
|
||||||
|
// SELECT: SELECT * FROM users WHERE id = $1
|
||||||
|
// UPDATE: UPDATE users SET status = $1 WHERE id = $2
|
||||||
|
// DELETE: DELETE FROM users WHERE id = $1
|
||||||
|
func basicCRUDExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
// --- INSERT ---
|
||||||
|
fmt.Println("\n--- Operasi INSERT ---")
|
||||||
|
insertData := query.InsertData{
|
||||||
|
Columns: []string{"name", "email", "status"},
|
||||||
|
Values: []interface{}{"Alice", "[email protected]", "active"},
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildInsertQuery("users", insertData, "id")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building INSERT: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated INSERT SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
result, err := qb.ExecuteInsert(ctx, db, "users", insertData, "id")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error INSERT: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newID, _ := result.LastInsertId()
|
||||||
|
fmt.Printf("-> INSERT: Berhasil menambah user dengan ID: %d\n", newID)
|
||||||
|
|
||||||
|
// --- SELECT (Single Row) ---
|
||||||
|
fmt.Println("\n--- Operasi SELECT ---")
|
||||||
|
var user User
|
||||||
|
selectQuery := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{{Expression: "*"}},
|
||||||
|
From: "users",
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
sql, args, err = qb.BuildQuery(selectQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building SELECT: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQueryRow(ctx, db, selectQuery, &user)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error SELECT single row: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> SELECT (Single Row): Berhasil mengambil user: %+v\n", user)
|
||||||
|
|
||||||
|
// --- UPDATE ---
|
||||||
|
fmt.Println("\n--- Operasi UPDATE ---")
|
||||||
|
updateData := query.UpdateData{
|
||||||
|
Columns: []string{"status"},
|
||||||
|
Values: []interface{}{"inactive"},
|
||||||
|
}
|
||||||
|
updateFilter := []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||||
|
}}
|
||||||
|
sql, args, err = qb.BuildUpdateQuery("users", updateData, updateFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building UPDATE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated UPDATE SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
_, err = qb.ExecuteUpdate(ctx, db, "users", updateData, updateFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error UPDATE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> UPDATE: Berhasil memperbarui status user dengan ID: %d\n", newID)
|
||||||
|
|
||||||
|
// --- DELETE ---
|
||||||
|
fmt.Println("\n--- Operasi DELETE ---")
|
||||||
|
deleteFilter := []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: newID}},
|
||||||
|
}}
|
||||||
|
sql, args, err = qb.BuildDeleteQuery("users", deleteFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building DELETE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated DELETE SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
_, err = qb.ExecuteDelete(ctx, db, "users", deleteFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error DELETE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> DELETE: Berhasil menghapus user dengan ID: %d\n", newID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 2: TRANSAKSI SQL (POSTGRESQL)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// sqlTransactionExample demonstrates how to perform atomic transactions involving updates
|
||||||
|
// across multiple tables using the Query Builder. It builds and prints SQL queries before execution.
|
||||||
|
// Expected output: Prints UPDATE SQL for salaries and employees, transaction commit/rollback status, and validation results.
|
||||||
|
// Example raw queries:
|
||||||
|
// UPDATE salaries: UPDATE salaries SET salary = $1 WHERE employee_id = $2
|
||||||
|
// UPDATE employees: UPDATE employees SET last_name = $1 WHERE employee_id = $2
|
||||||
|
func sqlTransactionExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
employeeID := 123
|
||||||
|
newSalary := 75000
|
||||||
|
newLastName := "Doe"
|
||||||
|
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Gagal mendapatkan koneksi database SQL: %v", err)
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
tx, err := db.BeginTxx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Gagal memulai transaksi SQL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
fmt.Println("Terjadi panic, melakukan rollback transaksi...")
|
||||||
|
_ = tx.Rollback()
|
||||||
|
panic(p)
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Printf("Transaksi dibatalkan (ROLLBACK) karena error: %v\n", err)
|
||||||
|
_ = tx.Rollback()
|
||||||
|
} else {
|
||||||
|
fmt.Println("Tidak ada error, melakukan COMMIT transaksi...")
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal melakukan COMMIT transaksi: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("Memulai transaksi untuk employee_id: %d\n", employeeID)
|
||||||
|
|
||||||
|
// --- Operasi 1: Update gaji di tabel 'salaries' ---
|
||||||
|
fmt.Println("\n--- Operasi 1: UPDATE salaries ---")
|
||||||
|
salariesUpdateData := query.UpdateData{
|
||||||
|
Columns: []string{"salary"},
|
||||||
|
Values: []interface{}{newSalary},
|
||||||
|
}
|
||||||
|
salariesFilter := []query.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "employee_id", Operator: query.OpEqual, Value: employeeID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildUpdateQuery("salaries", salariesUpdateData, salariesFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building UPDATE salaries: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated UPDATE salaries SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
salariesResult, err := qb.ExecuteUpdate(ctx, tx, "salaries", salariesUpdateData, salariesFilter)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
salariesRowsAffected, _ := salariesResult.RowsAffected()
|
||||||
|
fmt.Printf("-> UPDATE salaries: %d baris terpengaruh.\n", salariesRowsAffected)
|
||||||
|
|
||||||
|
// --- Operasi 2: Update informasi di tabel 'employees' ---
|
||||||
|
fmt.Println("\n--- Operasi 2: UPDATE employees ---")
|
||||||
|
employeesUpdateData := query.UpdateData{
|
||||||
|
Columns: []string{"last_name"},
|
||||||
|
Values: []interface{}{newLastName},
|
||||||
|
}
|
||||||
|
employeesFilter := []query.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "employee_id", Operator: query.OpEqual, Value: employeeID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sql, args, err = qb.BuildUpdateQuery("employees", employeesUpdateData, employeesFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building UPDATE employees: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated UPDATE employees SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
employeesResult, err := qb.ExecuteUpdate(ctx, tx, "employees", employeesUpdateData, employeesFilter)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
employeesRowsAffected, _ := employeesResult.RowsAffected()
|
||||||
|
fmt.Printf("-> UPDATE employees: %d baris terpengaruh.\n", employeesRowsAffected)
|
||||||
|
|
||||||
|
// --- Validasi Akhir Transaksi ---
|
||||||
|
if salariesRowsAffected == 1 && employeesRowsAffected == 1 {
|
||||||
|
fmt.Println("-> Validasi BERHASIL: Kedua tabel berhasil diperbarui.")
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("validasi GAGAL: diharapkan 1 baris terupdate di setiap tabel, tetapi mendapat %d (salaries) dan %d (employees)", salariesRowsAffected, employeesRowsAffected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 3: TRANSAKSI MONGODB
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// mongoTransactionExample demonstrates MongoDB transactions using the query builder.
|
||||||
|
// It prints the filters and update operations before executing them in a transaction.
|
||||||
|
// Expected output: Prints MongoDB filters and update operations for salaries and employees, transaction commit/abort status, and validation results.
|
||||||
|
// Example raw queries:
|
||||||
|
// MongoDB filters: {"employee_id": 123}
|
||||||
|
// MongoDB updates: {"$set": {"salary": 75000}}, {"$set": {"last_name": "Doe"}}
|
||||||
|
func mongoTransactionExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
employeeID := 123
|
||||||
|
newSalary := 75000
|
||||||
|
newLastName := "Doe"
|
||||||
|
|
||||||
|
client, err := dbService.GetMongoClient("mongodb")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Gagal mendapatkan klien MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
salariesCollection := client.Database("company_db").Collection("salaries")
|
||||||
|
employeesCollection := client.Database("company_db").Collection("employees")
|
||||||
|
|
||||||
|
session, err := client.StartSession()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Gagal memulai sesi MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
fmt.Printf("Memulai transaksi MongoDB untuk employee_id: %d\n", employeeID)
|
||||||
|
|
||||||
|
_, err = session.WithTransaction(ctx, func(sessCtx mongo.SessionContext) (interface{}, error) {
|
||||||
|
// --- Operasi 1: Update gaji di koleksi 'salaries' ---
|
||||||
|
fmt.Println("\n--- Operasi 1: UPDATE salaries ---")
|
||||||
|
salariesFilter := bson.M{"employee_id": employeeID}
|
||||||
|
salariesUpdate := bson.M{"$set": bson.M{"salary": newSalary}}
|
||||||
|
fmt.Printf("-> MongoDB Update Salaries Filter: %#v\n", salariesFilter)
|
||||||
|
fmt.Printf("-> MongoDB Update Salaries Operation: %#v\n", salariesUpdate)
|
||||||
|
|
||||||
|
salariesResult, err := salariesCollection.UpdateOne(sessCtx, salariesFilter, salariesUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gagal update koleksi salaries: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("-> UPDATE salaries: %d dokumen cocok (matched).\n", salariesResult.MatchedCount)
|
||||||
|
|
||||||
|
// --- Operasi 2: Update informasi di koleksi 'employees' ---
|
||||||
|
fmt.Println("\n--- Operasi 2: UPDATE employees ---")
|
||||||
|
employeesFilter := bson.M{"employee_id": employeeID}
|
||||||
|
employeesUpdate := bson.M{"$set": bson.M{"last_name": newLastName}}
|
||||||
|
fmt.Printf("-> MongoDB Update Employees Filter: %#v\n", employeesFilter)
|
||||||
|
fmt.Printf("-> MongoDB Update Employees Operation: %#v\n", employeesUpdate)
|
||||||
|
|
||||||
|
employeesResult, err := employeesCollection.UpdateOne(sessCtx, employeesFilter, employeesUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gagal update koleksi employees: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("-> UPDATE employees: %d dokumen cocok (matched).\n", employeesResult.MatchedCount)
|
||||||
|
|
||||||
|
// --- Validasi Akhir Transaksi ---
|
||||||
|
if salariesResult.MatchedCount == 1 && employeesResult.MatchedCount == 1 {
|
||||||
|
fmt.Println("-> Validasi BERHASIL: Kedua koleksi berhasil diperbarui.")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("validasi GAGAL: diharapkan 1 dokumen terupdate di setiap koleksi, tetapi mendapat %d (salaries) dan %d (employees)", salariesResult.MatchedCount, employeesResult.MatchedCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Transaksi MongoDB dibatalkan (ABORT) karena error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Transaksi MongoDB berhasil di-commit.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 4: FILTER DAN PAGINASI
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// filterAndPaginationExample demonstrates querying with filters and pagination.
|
||||||
|
// It builds and prints the SELECT query before executing it.
|
||||||
|
// Expected output: Prints SELECT SQL with filters and pagination, and the number of active users found.
|
||||||
|
// Example raw query:
|
||||||
|
// SELECT id, name FROM users WHERE (status = $1 AND created_at > $2) ORDER BY name ASC LIMIT 5 OFFSET 10
|
||||||
|
func filterAndPaginationExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
query := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "id"},
|
||||||
|
{Expression: "name"},
|
||||||
|
},
|
||||||
|
From: "users",
|
||||||
|
Filters: []query.FilterGroup{
|
||||||
|
{
|
||||||
|
LogicOp: "AND",
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "status", Operator: query.OpEqual, Value: "active"},
|
||||||
|
{Column: "created_at", Operator: query.OpGreaterThan, Value: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sort: []query.SortField{{Column: "name", Order: "ASC"}},
|
||||||
|
Limit: 5,
|
||||||
|
Offset: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []User
|
||||||
|
sql, args, err := qb.BuildQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building SELECT: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQuery(ctx, db, query, &users)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query dengan filter: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> Filter & Paginasi: Ditemukan %d user aktif (halaman 3).\n", len(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 5: QUERY DENGAN JOIN
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// joinExample demonstrates querying with JOIN operations.
|
||||||
|
// It builds and prints the JOIN query before executing it.
|
||||||
|
// Expected output: Prints JOIN SQL query and the number of posts with author names found.
|
||||||
|
// Example raw query:
|
||||||
|
// SELECT p.id AS post_id, p.title, u.name AS author_name FROM posts p INNER JOIN users u ON p.user_id = u.id LIMIT 10
|
||||||
|
func joinExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
query := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "p.id", Alias: "post_id"},
|
||||||
|
{Expression: "p.title"},
|
||||||
|
{Expression: "u.name", Alias: "author_name"},
|
||||||
|
},
|
||||||
|
From: "posts",
|
||||||
|
Aliases: "p",
|
||||||
|
Joins: []query.Join{
|
||||||
|
{
|
||||||
|
Type: "INNER",
|
||||||
|
Table: "users",
|
||||||
|
Alias: "u",
|
||||||
|
OnConditions: query.FilterGroup{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "p.user_id", Operator: query.OpEqual, Value: "u.id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
PostID int `db:"post_id"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
AuthorName string `db:"author_name"`
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building JOIN: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated JOIN SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query JOIN: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> JOIN: Ditemukan %d post dengan nama penulis.\n", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 6: QUERY DENGAN CTE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// cteExample demonstrates querying with Common Table Expressions (CTE).
|
||||||
|
// It builds and prints the CTE query before executing it.
|
||||||
|
// Expected output: Prints CTE SQL query and the number of users with more than 5 posts.
|
||||||
|
func cteExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
query := query.DynamicQuery{
|
||||||
|
CTEs: []query.CTE{
|
||||||
|
{
|
||||||
|
Name: "user_post_counts",
|
||||||
|
Query: query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "user_id"},
|
||||||
|
{Expression: "COUNT(*)", Alias: "post_count"},
|
||||||
|
},
|
||||||
|
From: "posts",
|
||||||
|
GroupBy: []string{"user_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "u.name"},
|
||||||
|
{Expression: "upc.post_count"},
|
||||||
|
},
|
||||||
|
From: "users u",
|
||||||
|
Joins: []query.Join{
|
||||||
|
{
|
||||||
|
Type: "INNER",
|
||||||
|
Table: "user_post_counts",
|
||||||
|
Alias: "upc",
|
||||||
|
OnConditions: query.FilterGroup{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "u.id", Operator: query.OpEqual, Value: "upc.user_id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []query.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "upc.post_count", Operator: query.OpGreaterThan, Value: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
Name string `db:"name"`
|
||||||
|
PostCount int `db:"post_count"`
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building CTE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated CTE SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query CTE: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> CTE: Ditemukan %d user dengan lebih dari 5 post.\n", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 7: WINDOW FUNCTION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// windowFunctionExample demonstrates querying with window functions.
|
||||||
|
// It builds and prints the window function query before executing it.
|
||||||
|
// Expected output: Prints window function SQL query and the number of employees with salary rankings.
|
||||||
|
func windowFunctionExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
query := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "name"},
|
||||||
|
{Expression: "department"},
|
||||||
|
{Expression: "salary"},
|
||||||
|
},
|
||||||
|
From: "employees",
|
||||||
|
WindowFunctions: []query.WindowFunction{
|
||||||
|
{
|
||||||
|
Function: "RANK",
|
||||||
|
Over: "department",
|
||||||
|
OrderBy: "salary DESC",
|
||||||
|
Alias: "salary_rank",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filters: []query.FilterGroup{
|
||||||
|
{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{Column: "department", Operator: query.OpEqual, Value: "Engineering"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
Name string `db:"name"`
|
||||||
|
Department string `db:"department"`
|
||||||
|
Salary float64 `db:"salary"`
|
||||||
|
SalaryRank int `db:"salary_rank"`
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building Window Function: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated Window Function SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQuery(ctx, db, query, &results)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query Window Function: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> Window Function: Ditemukan %d employee di departemen Engineering dengan peringkat gaji.\n", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 8: VALIDASI DATA DINAMIS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// validationExample demonstrates dynamic data validation using the query builder.
|
||||||
|
// It builds and prints the validation query before executing it.
|
||||||
|
// Expected output: Prints validation SQL query and whether the email is duplicate or available.
|
||||||
|
func validationExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
validator := validation.NewDynamicValidator(qb)
|
||||||
|
|
||||||
|
userData := map[string]interface{}{"email": "[email protected]"}
|
||||||
|
emailRule := validation.NewUniqueFieldRule("users", "email")
|
||||||
|
|
||||||
|
// Build and print the validation query
|
||||||
|
countQuery := query.DynamicQuery{
|
||||||
|
From: "users",
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{{Column: "email", Operator: query.OpEqual, Value: "[email protected]"}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
sql, args, err := qb.BuildCountQuery(countQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building validation query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated Validation SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
|
||||||
|
isDuplicate, err := validator.Validate(ctx, db, emailRule, userData)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error validasi: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDuplicate {
|
||||||
|
fmt.Println("-> Validasi: Email '[email protected]' sudah ada.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("-> Validasi: Email '[email protected]' tersedia.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 9: OPERASI JSON
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// jsonQueryExample demonstrates JSON operations in queries.
|
||||||
|
// It builds and prints the JSON queries before executing them.
|
||||||
|
// Expected output: Prints JSON SELECT and UPDATE SQL queries, number of employees found, and update success message.
|
||||||
|
func jsonQueryExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := dbService.GetSQLXDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan koneksi DB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
query := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{{Expression: "*"}},
|
||||||
|
From: "employees",
|
||||||
|
Filters: []query.FilterGroup{{
|
||||||
|
Filters: []query.DynamicFilter{
|
||||||
|
{
|
||||||
|
Column: "metadata",
|
||||||
|
Operator: query.OpJsonEqual,
|
||||||
|
Value: "Engineering",
|
||||||
|
Options: map[string]interface{}{"path": "department"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var employees []Employee
|
||||||
|
sql, args, err := qb.BuildQuery(query)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building JSON query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated JSON SELECT SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
err = qb.ExecuteQuery(ctx, db, query, &employees)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query JSON: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> Operasi JSON: Ditemukan %d employee di departemen Engineering (dari metadata JSON).\n", len(employees))
|
||||||
|
|
||||||
|
updateData := query.UpdateData{
|
||||||
|
JsonUpdates: map[string]query.JsonUpdate{
|
||||||
|
"metadata": {Path: "role", Value: "Senior Developer"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
filter := []query.FilterGroup{{Filters: []query.DynamicFilter{{Column: "id", Operator: query.OpEqual, Value: 1}}}}
|
||||||
|
sql, args, err = qb.BuildUpdateQuery("employees", updateData, filter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building JSON update: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated JSON UPDATE SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
_, err = qb.ExecuteUpdate(ctx, db, "employees", updateData, filter)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error update JSON: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("-> Operasi JSON: Berhasil memperbarui 'role' di metadata untuk employee ID 1.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 10: QUERY MONGODB
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// mongodbExample demonstrates MongoDB queries using the query builder.
|
||||||
|
// It prints the built filters and pipelines before executing them.
|
||||||
|
// Expected output: Prints MongoDB find filter, number of active users, aggregation pipeline, and number of departments.
|
||||||
|
func mongodbExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client, err := dbService.GetMongoClient("mongodb")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan klien MongoDB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collection := client.Database("company_db").Collection("users")
|
||||||
|
mqb := query.NewMongoQueryBuilder()
|
||||||
|
|
||||||
|
// --- FIND ---
|
||||||
|
fmt.Println("\n--- Operasi FIND ---")
|
||||||
|
findQuery := query.DynamicQuery{
|
||||||
|
Filters: []query.FilterGroup{{Filters: []query.DynamicFilter{{Column: "status", Operator: query.OpEqual, Value: "active"}}}},
|
||||||
|
Limit: 5,
|
||||||
|
}
|
||||||
|
filter, _, _ := mqb.BuildFindQuery(findQuery)
|
||||||
|
fmt.Printf("-> MongoDB Find Filter: %#v\n", filter)
|
||||||
|
|
||||||
|
var users []User
|
||||||
|
err = mqb.ExecuteFind(ctx, collection, findQuery, &users)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error MongoDB Find: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> MongoDB Find: Ditemukan %d user aktif.\n", len(users))
|
||||||
|
|
||||||
|
// --- AGGREGATION ---
|
||||||
|
fmt.Println("\n--- Operasi AGGREGATION ---")
|
||||||
|
aggQuery := query.DynamicQuery{
|
||||||
|
Fields: []query.SelectField{
|
||||||
|
{Expression: "department", Alias: "_id"},
|
||||||
|
{Expression: "COUNT(*)", Alias: "count"},
|
||||||
|
},
|
||||||
|
GroupBy: []string{"department"},
|
||||||
|
}
|
||||||
|
pipeline, _ := mqb.BuildAggregateQuery(aggQuery)
|
||||||
|
fmt.Printf("-> MongoDB Aggregation Pipeline: %#v\n", pipeline)
|
||||||
|
|
||||||
|
var aggResults []struct {
|
||||||
|
ID string `bson:"_id"`
|
||||||
|
Count int `bson:"count"`
|
||||||
|
}
|
||||||
|
err = mqb.ExecuteAggregate(ctx, collection, aggQuery, &aggResults)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error MongoDB Aggregate: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> MongoDB Aggregate: Ditemukan user di %d departemen.\n", len(aggResults))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 11: PENGGUNAAN READ REPLICA
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// readReplicaExample demonstrates using read replicas for queries.
|
||||||
|
// It builds and prints the count query before executing it on the read replica.
|
||||||
|
// Expected output: Prints COUNT SQL query and the total number of users from the read replica.
|
||||||
|
// Example raw query:
|
||||||
|
// SELECT COUNT(*) FROM users
|
||||||
|
func readReplicaExample(dbService database.Service) {
|
||||||
|
ctx := context.Background()
|
||||||
|
readDB, err := dbService.GetReadDB("main")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Gagal mendapatkan read replica: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readxDB := sqlx.NewDb(readDB, "pgx")
|
||||||
|
qb := query.NewQueryBuilder(query.DBTypePostgreSQL)
|
||||||
|
|
||||||
|
countQuery := query.DynamicQuery{From: "users"}
|
||||||
|
sql, args, err := qb.BuildCountQuery(countQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error building count query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Generated COUNT SQL: %s\nArgs: %v\n", sql, args)
|
||||||
|
count, err := qb.ExecuteCount(ctx, readxDB, countQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error query di read replica: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("-> Read Replica: Total user (dari read replica): %d\n", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 12: HEALTH CHECK DATABASE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// healthCheckExample demonstrates database health checks.
|
||||||
|
// It prints the health status of all databases.
|
||||||
|
// Expected output: Prints health status for each database (up/down with type or error).
|
||||||
|
func healthCheckExample(dbService database.Service) {
|
||||||
|
healthStatus := dbService.Health()
|
||||||
|
fmt.Println("-> Health Check Status:")
|
||||||
|
for dbName, status := range healthStatus {
|
||||||
|
if status["status"] == "up" {
|
||||||
|
fmt.Printf(" - Database %s: SEHAT (%s)\n", dbName, status["type"])
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" - Database %s: TIDAK SEHAT - %s\n", dbName, status["error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH 13: PARSING QUERY DARI URL
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// urlQueryParsingExample demonstrates parsing query parameters from URL.
|
||||||
|
// It parses the URL query and prints the resulting dynamic query structure.
|
||||||
|
// Expected output: Prints parsed fields, filters, sort, and limit from the URL query.
|
||||||
|
func urlQueryParsingExample(dbService database.Service) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("fields", "id,name")
|
||||||
|
values.Set("filter[status][_eq]", "active")
|
||||||
|
values.Set("filter[age][_gt]", "25")
|
||||||
|
values.Set("sort", "-name")
|
||||||
|
values.Set("limit", "10")
|
||||||
|
|
||||||
|
parser := query.NewQueryParser()
|
||||||
|
dynamicQuery, err := parser.ParseQuery(values, "users")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error parsing URL query: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("-> Parsing URL Query:")
|
||||||
|
fmt.Printf(" Fields: %v\n", dynamicQuery.Fields)
|
||||||
|
fmt.Printf(" Filters: %+v\n", dynamicQuery.Filters)
|
||||||
|
fmt.Printf(" Sort: %+v\n", dynamicQuery.Sort)
|
||||||
|
fmt.Printf(" Limit: %d\n", dynamicQuery.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AKHIR FILE
|
||||||
|
// =============================================================================
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
queryUtils "antrian-operasi/internal/utils/query"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DYNAMIC VALIDATION RULE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ValidationRule mendefinisikan aturan untuk memeriksa duplikat atau kondisi lain.
|
||||||
|
// Struct ini membuat validator dapat digunakan kembali untuk tabel apa pun.
|
||||||
|
type ValidationRule struct {
|
||||||
|
// TableName adalah nama tabel yang akan diperiksa.
|
||||||
|
TableName string
|
||||||
|
|
||||||
|
// UniqueColumns adalah daftar kolom yang, jika digabungkan, harus unik.
|
||||||
|
// Contoh: []string{"email"} atau []string{"first_name", "last_name", "dob"}
|
||||||
|
UniqueColumns []string
|
||||||
|
|
||||||
|
// Conditions adalah filter tambahan yang harus dipenuhi.
|
||||||
|
// Ini sangat berguna untuk aturan bisnis, seperti "status != 'deleted'".
|
||||||
|
// Gunakan queryUtils.DynamicFilter untuk fleksibilitas penuh.
|
||||||
|
Conditions []queryUtils.DynamicFilter
|
||||||
|
|
||||||
|
// ExcludeIDColumn dan ExcludeIDValue digunakan untuk operasi UPDATE,
|
||||||
|
// untuk memastikan bahwa record tidak membandingkan dirinya sendiri.
|
||||||
|
ExcludeIDColumn string
|
||||||
|
ExcludeIDValue interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUniqueFieldRule adalah helper untuk membuat aturan validasi unik untuk satu kolom.
|
||||||
|
// Ini adalah cara cepat untuk membuat aturan yang paling umum.
|
||||||
|
func NewUniqueFieldRule(tableName, uniqueColumn string, additionalConditions ...queryUtils.DynamicFilter) ValidationRule {
|
||||||
|
return ValidationRule{
|
||||||
|
TableName: tableName,
|
||||||
|
UniqueColumns: []string{uniqueColumn},
|
||||||
|
Conditions: additionalConditions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DYNAMIC VALIDATOR
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// DynamicValidator menyediakan metode untuk menjalankan validasi berdasarkan ValidationRule.
|
||||||
|
// Ini sepenuhnya generik dan tidak terikat pada tabel atau model tertentu.
|
||||||
|
type DynamicValidator struct {
|
||||||
|
qb *queryUtils.QueryBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDynamicValidator membuat instance DynamicValidator baru.
|
||||||
|
func NewDynamicValidator(qb *queryUtils.QueryBuilder) *DynamicValidator {
|
||||||
|
return &DynamicValidator{qb: qb}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate menjalankan validasi terhadap aturan yang diberikan.
|
||||||
|
// `data` adalah map yang berisi nilai untuk kolom yang akan diperiksa (biasanya dari request body).
|
||||||
|
// Mengembalikan `true` jika ada duplikat yang ditemukan (validasi gagal), `false` jika tidak ada duplikat (validasi berhasil).
|
||||||
|
func (dv *DynamicValidator) Validate(ctx context.Context, db *sqlx.DB, rule ValidationRule, data map[string]interface{}) (bool, error) {
|
||||||
|
// LOGGING: Log validation start
|
||||||
|
fmt.Printf("[VALIDATION] Starting validation for table: %s, unique columns: %v, data: %v\n", rule.TableName, rule.UniqueColumns, data)
|
||||||
|
|
||||||
|
if len(rule.UniqueColumns) == 0 {
|
||||||
|
fmt.Printf("[VALIDATION] ERROR: ValidationRule must have at least one UniqueColumn\n")
|
||||||
|
return false, fmt.Errorf("ValidationRule must have at least one UniqueColumn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Kumpulkan semua filter dari aturan
|
||||||
|
var allFilters []queryUtils.DynamicFilter
|
||||||
|
|
||||||
|
// Tambahkan kondisi tambahan (misalnya, status != 'deleted')
|
||||||
|
allFilters = append(allFilters, rule.Conditions...)
|
||||||
|
fmt.Printf("[VALIDATION] Added %d condition filters\n", len(rule.Conditions))
|
||||||
|
|
||||||
|
// 2. Bangun filter untuk kolom unik berdasarkan data yang diberikan
|
||||||
|
for _, colName := range rule.UniqueColumns {
|
||||||
|
value, exists := data[colName]
|
||||||
|
if !exists {
|
||||||
|
// Jika data untuk kolom unik tidak ada, ini adalah kesalahan pemrograman.
|
||||||
|
fmt.Printf("[VALIDATION] ERROR: data for unique column '%s' not found in provided data map\n", colName)
|
||||||
|
return false, fmt.Errorf("data for unique column '%s' not found in provided data map", colName)
|
||||||
|
}
|
||||||
|
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||||
|
Column: colName,
|
||||||
|
Operator: queryUtils.OpEqual,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
fmt.Printf("[VALIDATION] Added filter for column '%s' with value: %v\n", colName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tambahkan filter pengecualian ID (untuk operasi UPDATE)
|
||||||
|
if rule.ExcludeIDColumn != "" {
|
||||||
|
allFilters = append(allFilters, queryUtils.DynamicFilter{
|
||||||
|
Column: rule.ExcludeIDColumn,
|
||||||
|
Operator: queryUtils.OpNotEqual,
|
||||||
|
Value: rule.ExcludeIDValue,
|
||||||
|
})
|
||||||
|
fmt.Printf("[VALIDATION] Added exclude filter for column '%s' with value: %v\n", rule.ExcludeIDColumn, rule.ExcludeIDValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Bangun dan eksekusi query untuk menghitung jumlah record yang cocok
|
||||||
|
query := queryUtils.DynamicQuery{
|
||||||
|
From: rule.TableName,
|
||||||
|
Filters: []queryUtils.FilterGroup{{Filters: allFilters, LogicOp: "AND"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[VALIDATION] Built query with %d total filters\n", len(allFilters))
|
||||||
|
|
||||||
|
count, err := dv.qb.ExecuteCount(ctx, db, query)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[VALIDATION] ERROR: failed to execute validation query for table %s: %v\n", rule.TableName, err)
|
||||||
|
return false, fmt.Errorf("failed to execute validation query for table %s: %w", rule.TableName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[VALIDATION] Query executed successfully, count result: %d\n", count)
|
||||||
|
|
||||||
|
// 5. Kembalikan hasil
|
||||||
|
result := count > 0
|
||||||
|
fmt.Printf("[VALIDATION] Validation result: isDuplicate=%t (count > 0: %d > 0 = %t)\n", result, count, result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTOH PENGGUNAAN (UNTUK DITEMPATKAN DI HANDLER ANDA)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
// --- Cara Penggunaan di RetribusiHandler ---
|
||||||
|
|
||||||
|
// 1. Tambahkan DynamicValidator ke struct handler
|
||||||
|
type RetribusiHandler struct {
|
||||||
|
// ...
|
||||||
|
validator *validation.DynamicValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Inisialisasi di constructor
|
||||||
|
func NewRetribusiHandler() *RetribusiHandler {
|
||||||
|
qb := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).SetAllowedColumns(...)
|
||||||
|
|
||||||
|
return &RetribusiHandler{
|
||||||
|
// ...
|
||||||
|
validator: validation.NewDynamicValidator(qb),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Gunakan di CreateRetribusi
|
||||||
|
func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) {
|
||||||
|
var req retribusi.RetribusiCreateRequest
|
||||||
|
// ... bind dan validasi request ...
|
||||||
|
|
||||||
|
// Siapkan aturan validasi: KodeTarif harus unik di antara record yang tidak dihapus.
|
||||||
|
rule := validation.NewUniqueFieldRule(
|
||||||
|
"data_retribusi", // Nama tabel
|
||||||
|
"Kode_tarif", // Kolom yang harus unik
|
||||||
|
queryUtils.DynamicFilter{ // Kondisi tambahan
|
||||||
|
Column: "status",
|
||||||
|
Operator: queryUtils.OpNotEqual,
|
||||||
|
Value: "deleted",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Siapkan data dari request untuk divalidasi
|
||||||
|
dataToValidate := map[string]interface{}{
|
||||||
|
"Kode_tarif": req.KodeTarif,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eksekusi validasi
|
||||||
|
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDuplicate {
|
||||||
|
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... lanjutkan proses create ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Gunakan di UpdateRetribusi
|
||||||
|
func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var req retribusi.RetribusiUpdateRequest
|
||||||
|
// ... bind dan validasi request ...
|
||||||
|
|
||||||
|
// Siapkan aturan validasi: KodeTarif harus unik, kecuali untuk record dengan ID ini.
|
||||||
|
rule := validation.ValidationRule{
|
||||||
|
TableName: "data_retribusi",
|
||||||
|
UniqueColumns: []string{"Kode_tarif"},
|
||||||
|
Conditions: []queryUtils.DynamicFilter{
|
||||||
|
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
|
||||||
|
},
|
||||||
|
ExcludeIDColumn: "id", // Kecualikan berdasarkan kolom 'id'
|
||||||
|
ExcludeIDValue: id, // ...dengan nilai ID dari parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
dataToValidate := map[string]interface{}{
|
||||||
|
"Kode_tarif": req.KodeTarif,
|
||||||
|
}
|
||||||
|
|
||||||
|
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Failed to validate Kode Tarif", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDuplicate {
|
||||||
|
h.respondError(c, "Kode Tarif already exists", fmt.Errorf("duplicate Kode Tarif: %s", req.KodeTarif), http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... lanjutkan proses update ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Contoh Penggunaan untuk Kasus Lain ---
|
||||||
|
|
||||||
|
// Contoh: Validasi kombinasi unik untuk tabel 'users'
|
||||||
|
// (email dan company_id harus unik bersama-sama)
|
||||||
|
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
rule := validation.ValidationRule{
|
||||||
|
TableName: "users",
|
||||||
|
UniqueColumns: []string{"email", "company_id"}, // Unik komposit
|
||||||
|
}
|
||||||
|
|
||||||
|
dataToValidate := map[string]interface{}{
|
||||||
|
"email": req.Email,
|
||||||
|
"company_id": req.CompanyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
|
||||||
|
// ... handle error dan duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5 (class 2615 OID 18291)
|
||||||
|
-- Name: public; Type: SCHEMA; Schema: -; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
-- *not* creating schema, since initdb creates it
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SCHEMA public OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5092 (class 0 OID 0)
|
||||||
|
-- Dependencies: 5
|
||||||
|
-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON SCHEMA public IS '';
|
||||||
|
|
||||||
|
|
||||||
|
SET default_tablespace = '';
|
||||||
|
|
||||||
|
SET default_table_access_method = heap;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 220 (class 1259 OID 18294)
|
||||||
|
-- Name: daftar_kategori_operasi; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.daftar_kategori_operasi (
|
||||||
|
id integer NOT NULL,
|
||||||
|
status character varying(255) DEFAULT 'draft'::character varying NOT NULL,
|
||||||
|
date_created timestamp without time zone,
|
||||||
|
date_updated timestamp without time zone,
|
||||||
|
"Kategori" character varying(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.daftar_kategori_operasi OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 219 (class 1259 OID 18293)
|
||||||
|
-- Name: daftar_kategori_operasi_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.daftar_kategori_operasi_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SEQUENCE public.daftar_kategori_operasi_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5094 (class 0 OID 0)
|
||||||
|
-- Dependencies: 219
|
||||||
|
-- Name: daftar_kategori_operasi_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.daftar_kategori_operasi_id_seq OWNED BY public.daftar_kategori_operasi.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 233 (class 1259 OID 18376)
|
||||||
|
-- Name: daftar_ksm; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.daftar_ksm (
|
||||||
|
id integer NOT NULL,
|
||||||
|
"Nama_ksm" character varying(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.daftar_ksm OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 232 (class 1259 OID 18375)
|
||||||
|
-- Name: daftar_ksm_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.daftar_ksm_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SEQUENCE public.daftar_ksm_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5095 (class 0 OID 0)
|
||||||
|
-- Dependencies: 232
|
||||||
|
-- Name: daftar_ksm_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.daftar_ksm_id_seq OWNED BY public.daftar_ksm.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 223 (class 1259 OID 18317)
|
||||||
|
-- Name: daftar_spesialis; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.daftar_spesialis (
|
||||||
|
id integer NOT NULL,
|
||||||
|
"Kode" character varying(255),
|
||||||
|
"Spesialis" character varying(255),
|
||||||
|
"Id_group_location_simgos" character varying(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.daftar_spesialis OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 222 (class 1259 OID 18316)
|
||||||
|
-- Name: daftar_spesialis_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.daftar_spesialis_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SEQUENCE public.daftar_spesialis_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5096 (class 0 OID 0)
|
||||||
|
-- Dependencies: 222
|
||||||
|
-- Name: daftar_spesialis_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.daftar_spesialis_id_seq OWNED BY public.daftar_spesialis.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 225 (class 1259 OID 18327)
|
||||||
|
-- Name: daftar_subspesialis; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.daftar_subspesialis (
|
||||||
|
id integer NOT NULL,
|
||||||
|
"Kode" character varying(255),
|
||||||
|
"Subspesialis" character varying(255),
|
||||||
|
"FK_daftar_spesialis_ID" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.daftar_subspesialis OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 224 (class 1259 OID 18326)
|
||||||
|
-- Name: daftar_subspesialis_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.daftar_subspesialis_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SEQUENCE public.daftar_subspesialis_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5097 (class 0 OID 0)
|
||||||
|
-- Dependencies: 224
|
||||||
|
-- Name: daftar_subspesialis_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.daftar_subspesialis_id_seq OWNED BY public.daftar_subspesialis.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 230 (class 1259 OID 18358)
|
||||||
|
-- Name: data_diagnosa_pasien_operasi; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_diagnosa_pasien_operasi (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_created uuid,
|
||||||
|
date_created timestamp with time zone,
|
||||||
|
user_updated uuid,
|
||||||
|
date_updated timestamp with time zone,
|
||||||
|
"Kode_diagnosa" character varying(255),
|
||||||
|
"Diagnosa" character varying(255),
|
||||||
|
"Keterangan" character varying(255),
|
||||||
|
"FK_pasien_operasi_diagnosa_pasien_operasi_ID" uuid,
|
||||||
|
"Jenis_diagnosa" character varying(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_diagnosa_pasien_operasi OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 221 (class 1259 OID 18305)
|
||||||
|
-- Name: data_pasien_operasi; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_pasien_operasi (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
status character varying(20) DEFAULT 'draft'::character varying NOT NULL,
|
||||||
|
sort integer,
|
||||||
|
user_created uuid,
|
||||||
|
date_created timestamp with time zone,
|
||||||
|
user_updated uuid,
|
||||||
|
date_updated timestamp with time zone,
|
||||||
|
"No_rekam_medis" character varying(255),
|
||||||
|
"No_KTP" character varying(255),
|
||||||
|
"Nama_pasien" character varying(255),
|
||||||
|
"Jenis_kelamin" character varying(255),
|
||||||
|
"Tanggal_lahir" date,
|
||||||
|
"Umur" character varying(255),
|
||||||
|
"Alamat" text,
|
||||||
|
"Tanggal_daftar" timestamp without time zone,
|
||||||
|
"Kategori_operasi" integer,
|
||||||
|
"Rencana_operasi" text,
|
||||||
|
"Keterangan" text,
|
||||||
|
"Tanggal_selesai_operasi" timestamp without time zone,
|
||||||
|
"Status_operasi" character varying(255),
|
||||||
|
"Nomor" integer,
|
||||||
|
"Old_kategori" integer,
|
||||||
|
"Spesialis" integer,
|
||||||
|
"Sub_spesialis" integer NOT NULL,
|
||||||
|
"Keterangan_status_pasien" text,
|
||||||
|
"Nomor_spesialis" integer,
|
||||||
|
"Nomor_sub_spesialis" integer,
|
||||||
|
"Old_spesialis" integer,
|
||||||
|
"Old_sub_spesialis" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_pasien_operasi OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 229 (class 1259 OID 18351)
|
||||||
|
-- Name: data_pasien_operasi_data_pegawai; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_pasien_operasi_data_pegawai (
|
||||||
|
id integer NOT NULL,
|
||||||
|
data_pasien_operasi_id uuid,
|
||||||
|
data_pegawai_id uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_pasien_operasi_data_pegawai OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 228 (class 1259 OID 18350)
|
||||||
|
-- Name: data_pasien_operasi_data_pegawai_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.data_pasien_operasi_data_pegawai_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER SEQUENCE public.data_pasien_operasi_data_pegawai_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 5098 (class 0 OID 0)
|
||||||
|
-- Dependencies: 228
|
||||||
|
-- Name: data_pasien_operasi_data_pegawai_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.data_pasien_operasi_data_pegawai_id_seq OWNED BY public.data_pasien_operasi_data_pegawai.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 231 (class 1259 OID 18366)
|
||||||
|
-- Name: data_pegawai; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_pegawai (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
status character varying(255) DEFAULT 'draft'::character varying,
|
||||||
|
sort integer,
|
||||||
|
user_created uuid,
|
||||||
|
date_created timestamp with time zone,
|
||||||
|
user_updated uuid,
|
||||||
|
date_updated timestamp with time zone,
|
||||||
|
"Foto_pegawai" uuid,
|
||||||
|
"Kartu_pegawai" uuid,
|
||||||
|
"KTP" uuid,
|
||||||
|
"Nomor_kartu_pegawai" character varying(255),
|
||||||
|
"NIP" character varying(255),
|
||||||
|
"NIP_lama" character varying(255),
|
||||||
|
"Jabatan" integer,
|
||||||
|
"Golongan" integer,
|
||||||
|
"Rumpun" integer,
|
||||||
|
"KSM" integer,
|
||||||
|
"Tanggal_masuk" date,
|
||||||
|
"Tanggal_pensiun" date,
|
||||||
|
"TMT_CPNS" date,
|
||||||
|
"TMT_PNS" date,
|
||||||
|
"Satuan_kerja" integer,
|
||||||
|
"FK_spt_pegawai_id" uuid,
|
||||||
|
"Nama_depan" character varying(255),
|
||||||
|
"Nama_belakang" character varying(255),
|
||||||
|
"Username" character varying(255),
|
||||||
|
"User_email" character varying(255),
|
||||||
|
"Id_user" character varying(255),
|
||||||
|
"Practicioner" uuid,
|
||||||
|
"HFIS_code" character varying(255),
|
||||||
|
"HFIS_display" character varying(255),
|
||||||
|
"Code_dpjp" character varying(255),
|
||||||
|
"KDDOKTER" bigint,
|
||||||
|
"Practicioner_detail" bigint,
|
||||||
|
"Kode_satusehat" character varying(255),
|
||||||
|
"No_whatsapp" character varying(255),
|
||||||
|
"Posisi_ruang" integer,
|
||||||
|
"Substansi" integer,
|
||||||
|
"NDE" uuid,
|
||||||
|
"Subspesialis" integer,
|
||||||
|
"Status_pegawai" integer,
|
||||||
|
"File_status" uuid,
|
||||||
|
"Ketarangan" integer,
|
||||||
|
"Kode_DPJP" character varying(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_pegawai OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 226 (class 1259 OID 18336)
|
||||||
|
-- Name: data_telepon_pasien_operasi; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_telepon_pasien_operasi (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
"Nomor_telepon" character varying(255),
|
||||||
|
"FK_pasien_operasi_telepon_pasien_operasi_ID" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_telepon_pasien_operasi OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 227 (class 1259 OID 18342)
|
||||||
|
-- Name: data_tindakan_pasien_operasi; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.data_tindakan_pasien_operasi (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_created uuid,
|
||||||
|
date_created timestamp with time zone,
|
||||||
|
user_updated uuid,
|
||||||
|
date_updated timestamp with time zone,
|
||||||
|
"Kode_tindakan" character varying(255),
|
||||||
|
"Tindakan" character varying(255),
|
||||||
|
"Tindakan_tambahan" text,
|
||||||
|
"FK_pasien_operasi_tindakan_pasien_operasi_ID" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--ALTER TABLE public.data_tindakan_pasien_operasi OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 4896 (class 2604 OID 18297)
|
||||||
|
-- Name: daftar_kategori_operasi id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.daftar_kategori_operasi ALTER COLUMN id SET DEFAULT nextval('public.daftar_kategori_operasi_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 4903 (class 2604 OID 18379)
|
||||||
|
-- Name: daftar_ksm id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.daftar_ksm ALTER COLUMN id SET DEFAULT nextval('public.daftar_ksm_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 4899 (class 2604 OID 18320)
|
||||||
|
-- Name: daftar_spesialis id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.daftar_spesialis ALTER COLUMN id SET DEFAULT nextval('public.daftar_spesialis_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 4900 (class 2604 OID 18330)
|
||||||
|
-- Name: daftar_subspesialis id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.daftar_subspesialis ALTER COLUMN id SET DEFAULT nextval('public.daftar_subspesialis_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 4901 (class 2604 OID 18354)
|
||||||
|
-- Name: data_pasien_operasi_data_pegawai id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.data_pasien_operasi_data_pegawai ALTER COLUMN id SET DEFAULT nextval('public.data_pasien_operasi_data_pegawai_id_seq'::regclass);
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
INSERT INTO public.daftar_kategori_operasi
|
||||||
|
(id, status, date_created, date_updated, "Kategori")
|
||||||
|
VALUES
|
||||||
|
(1,'published','2026-01-23 08:22:12.314894',NULL,'1.1 Bedah Umum'),
|
||||||
|
(2,'published','2026-01-23 08:22:12.314894',NULL,'1.2 Bedah Syaraf'),
|
||||||
|
(3,'published','2026-01-23 08:22:12.314894',NULL,'1.3 Orthopedi'),
|
||||||
|
(4,'published','2026-01-23 08:22:12.314894',NULL,'1.4 Obgyn'),
|
||||||
|
(5,'published','2026-01-23 08:22:12.314894',NULL,'1.5 Bedah Plastik');
|
||||||
|
|
||||||
|
INSERT INTO public.daftar_ksm (id, "Nama_ksm")
|
||||||
|
VALUES
|
||||||
|
(1, 'KSM Bedah'),
|
||||||
|
(2, 'KSM Anestesi'),
|
||||||
|
(3, 'KSM Penyakit Dalam');
|
||||||
|
|
||||||
|
INSERT INTO public.daftar_spesialis
|
||||||
|
(id, "Kode", "Spesialis", "Id_group_location_simgos")
|
||||||
|
VALUES
|
||||||
|
(1, 'SPB', 'Spesialis Bedah', NULL),
|
||||||
|
(2, 'SPOT', 'Spesialis Orthopedi', NULL),
|
||||||
|
(3, 'SPM', 'Spesialis Mata', NULL);
|
||||||
|
|
||||||
|
INSERT INTO public.daftar_subspesialis
|
||||||
|
(id, "Kode", "Subspesialis", "FK_daftar_spesialis_ID")
|
||||||
|
VALUES
|
||||||
|
(1, 'SUB-DIG', 'Bedah Digestif', 1),
|
||||||
|
(2, 'SUB-ONK', 'Bedah Onkologi', 1),
|
||||||
|
(3, 'SUB-SPINE', 'Tulang Belakang', 2),
|
||||||
|
(4, 'SUB-HIP', 'Panggul & Lutut', 2),
|
||||||
|
(5, 'SUB-RET', 'Retina', 3);
|
||||||
|
|
||||||
|
INSERT INTO public.data_diagnosa_pasien_operasi
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
user_created,
|
||||||
|
date_created,
|
||||||
|
user_updated,
|
||||||
|
date_updated,
|
||||||
|
"Kode_diagnosa",
|
||||||
|
"Diagnosa",
|
||||||
|
"Keterangan",
|
||||||
|
"FK_pasien_operasi_diagnosa_pasien_operasi_ID",
|
||||||
|
"Jenis_diagnosa"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('8a1cdcbb-b5a3-4766-acaf-14c079768852', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-98', 'Hernia Inguinalis', NULL, '54b80a14-e557-482a-9c02-cf43312fe2b0', 'Utama'),
|
||||||
|
('2fe173de-929d-48e5-8df6-b364b6fb82a9', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-95', 'Tumor Mammae', NULL, '441d2ffb-7167-431c-aed2-c28069c88e56', 'Utama'),
|
||||||
|
('3ef80141-c98e-452a-b13e-f42e57cb2ce4', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-6', 'Appendicitis Akut', NULL, '8d995e32-56da-493f-9fdf-f7adba543e68', 'Utama'),
|
||||||
|
('63ab8f85-ebf8-4db0-bbc4-486719ed9bdf', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-85', 'Tonsilitis', NULL, '4ad135b4-6f85-463d-86d5-4bd5347b5122', 'Utama'),
|
||||||
|
('72dda5ec-2531-49f5-9fb7-7f4b4d910c42', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-45', 'Tonsilitis', NULL, '9ca415b2-a22d-4886-a0ff-d23a16c493a5', 'Utama'),
|
||||||
|
('26d604e8-ed6b-4aad-9535-e3316ee1679e', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-66', 'Hemoroid', NULL, '5fe0d3b3-243c-4323-996e-856122dd1483', 'Utama'),
|
||||||
|
('2c1fada6-bc19-4c51-8ed6-d31d7e1300d9', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-76', 'Fraktur Femur', NULL, 'd98a42fe-69e6-4e85-83eb-2ed9bec79462', 'Utama'),
|
||||||
|
('19bcf454-b77f-44c1-b9d2-0315071d8e98', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-64', 'Appendicitis Akut', NULL, 'ddcef114-7ffc-4d5b-88d8-9229904ce2f3', 'Utama'),
|
||||||
|
('299cbcfa-a052-498c-a74d-fd95e2f346b6', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-75', 'Hemoroid', NULL, '46f428fc-03cf-4977-ab56-382132c17c66', 'Utama'),
|
||||||
|
('925b88c4-0f0b-4e57-a027-a5414ba64828', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-23', 'Hernia Inguinalis', NULL, '2f34b2f0-79ef-49de-b92b-e7b3b181a513', 'Utama'),
|
||||||
|
('0549368c-ce8d-422c-91e1-4aebe5d2b630', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-65', 'Caesar Section', NULL, '0c244c0f-4f04-41cd-958d-c53f1dbfaa8d', 'Utama'),
|
||||||
|
('6f16f291-6629-4136-bc78-06c7b4470322', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-64', 'Tonsilitis', NULL, 'f11f320f-663d-412a-9bea-eb4f9c306b4c', 'Utama'),
|
||||||
|
('70c963ca-7806-49da-a127-03e57b6d335b', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-6', 'Hernia Inguinalis', NULL, 'a636d329-08a0-4487-8660-39820bafb845', 'Utama'),
|
||||||
|
('f8b78a00-ee3e-4113-b6c4-2d19e8a3acc4', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-56', 'Katarak', NULL, '023aa46b-f669-4be8-9917-bd61db3d014d', 'Utama'),
|
||||||
|
('b259bbfd-be7f-434f-bbd6-a05ea06e6619', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-74', 'Batu Empedu', NULL, '6cce5ca3-0565-437a-bdd7-8981a5198e47', 'Utama'),
|
||||||
|
('c6d2db45-b13c-46b3-a239-a9af27d1bab7', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-10-92', 'Fraktur Femur', NULL, 'b525e1e5-6e6d-4e75-8359-1d66765a7b83', 'Utama');
|
||||||
|
|
||||||
|
INSERT INTO public.data_pasien_operasi
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
sort,
|
||||||
|
user_created,
|
||||||
|
date_created,
|
||||||
|
user_updated,
|
||||||
|
date_updated,
|
||||||
|
"No_rekam_medis",
|
||||||
|
"No_KTP",
|
||||||
|
"Nama_pasien",
|
||||||
|
"Jenis_kelamin",
|
||||||
|
"Tanggal_lahir",
|
||||||
|
"Umur",
|
||||||
|
"Alamat",
|
||||||
|
"Tanggal_daftar",
|
||||||
|
"Kategori_operasi",
|
||||||
|
"Rencana_operasi",
|
||||||
|
"Keterangan",
|
||||||
|
"Tanggal_selesai_operasi",
|
||||||
|
"Status_operasi",
|
||||||
|
"Nomor",
|
||||||
|
"Old_kategori",
|
||||||
|
"Spesialis",
|
||||||
|
"Sub_spesialis",
|
||||||
|
"Keterangan_status_pasien",
|
||||||
|
"Nomor_spesialis",
|
||||||
|
"Nomor_sub_spesialis",
|
||||||
|
"Old_spesialis",
|
||||||
|
"Old_sub_spesialis"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('54b80a14-e557-482a-9c02-cf43312fe2b0','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000001','3574160117775539','Agus Simanjuntak','Laki-laki','1998-02-16','22 Tahun','Jl. Gatot Subroto No.51','2026-01-23 08:22:48.003155',4,'Hemorrhoidectomy',NULL,NULL,'Terjadwal',1,NULL,2,3,NULL,2,3,NULL,NULL),
|
||||||
|
('441d2ffb-7167-431c-aed2-c28069c88e56','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000002','3572712812442323','Lina Setiawan','Laki-laki','1974-09-08','22 Tahun','Jl. Merdeka No.21','2026-01-23 08:22:48.003155',4,'Hemorrhoidectomy',NULL,NULL,'Terjadwal',2,NULL,1,1,NULL,1,1,NULL,NULL),
|
||||||
|
('8d995e32-56da-493f-9fdf-f7adba543e68','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000003','3573658249713122','Hana Rahmawati','Laki-laki','2018-07-01','27 Tahun','Jl. Sudirman No.19','2026-01-23 08:22:48.003155',3,'Mastectomy',NULL,NULL,'Terjadwal',3,NULL,1,2,NULL,1,2,NULL,NULL),
|
||||||
|
('4ad135b4-6f85-463d-86d5-4bd5347b5122','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000004','3575264211898563','Nina Hidayat','Laki-laki','2004-07-03','62 Tahun','Jl. Ahmad Yani No.29','2026-01-23 08:22:48.003155',4,'Tonsillectomy',NULL,NULL,'Terjadwal',4,NULL,3,5,NULL,3,5,NULL,NULL),
|
||||||
|
('9ca415b2-a22d-4886-a0ff-d23a16c493a5','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000005','3578971794776205','Yudi Susanti','Laki-laki','2005-05-21','53 Tahun','Jl. Gatot Subroto No.27','2026-01-23 08:22:48.003155',2,'Phacoemulsification',NULL,NULL,'Terjadwal',5,NULL,1,2,NULL,1,2,NULL,NULL),
|
||||||
|
('5fe0d3b3-243c-4323-996e-856122dd1483','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000006','3577087502020920','Oscar Kusuma','Laki-laki','2023-04-20','26 Tahun','Jl. Gatot Subroto No.85','2026-01-23 08:22:48.003155',2,'Herniotomy',NULL,NULL,'Terjadwal',6,NULL,2,4,NULL,2,4,NULL,NULL),
|
||||||
|
('d98a42fe-69e6-4e85-83eb-2ed9bec79462','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000007','3574004915616037','Oscar Pratama','Laki-laki','1985-05-23','65 Tahun','Jl. Diponegoro No.99','2026-01-23 08:22:48.003155',3,'ORIF',NULL,NULL,'Terjadwal',7,NULL,2,4,NULL,2,4,NULL,NULL),
|
||||||
|
('ddcef114-7ffc-4d5b-88d8-9229904ce2f3','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000008','3578978139931591','Oscar Lestari','Laki-laki','1971-11-30','62 Tahun','Jl. Gatot Subroto No.28','2026-01-23 08:22:48.003155',2,'Phacoemulsification',NULL,NULL,'Terjadwal',8,NULL,3,5,NULL,3,5,NULL,NULL),
|
||||||
|
('46f428fc-03cf-4977-ab56-382132c17c66','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000009','3575019167672450','Siti Wijaya','Laki-laki','2020-06-06','19 Tahun','Jl. Diponegoro No.17','2026-01-23 08:22:48.003155',3,'Sectio Caesarea',NULL,NULL,'Terjadwal',9,NULL,2,4,NULL,2,4,NULL,NULL),
|
||||||
|
('2f34b2f0-79ef-49de-b92b-e7b3b181a513','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000010','3575876121081869','Nina Santoso','Laki-laki','2000-03-11','36 Tahun','Jl. Diponegoro No.90','2026-01-23 08:22:48.003155',2,'Herniotomy',NULL,NULL,'Terjadwal',10,NULL,1,2,NULL,1,2,NULL,NULL),
|
||||||
|
('0c244c0f-4f04-41cd-958d-c53f1dbfaa8d','waiting',NULL,NULL,'2026-01-23 08:22:48.003155+07',NULL,NULL,'RM-000011','3571621494638527','Oscar Simanjuntak','Laki-laki','2013-12-11','24 Tahun','Jl. Gatot Subroto No.57','2026-01-23 08:22:48.003155',4,'ORIF',NULL,NULL,'Terjadwal',11,NULL,2,3,NULL,2,3,NULL,NULL);
|
||||||
|
|
||||||
|
INSERT INTO public.data_pasien_operasi_data_pegawai
|
||||||
|
(id, data_pasien_operasi_id, data_pegawai_id)
|
||||||
|
VALUES
|
||||||
|
(1, '54b80a14-e557-482a-9c02-cf43312fe2b0', 'b4c116a4-6201-4e00-9b55-2bfe77ac262f'),
|
||||||
|
(2, '441d2ffb-7167-431c-aed2-c28069c88e56', 'a6b02554-ab33-4e9d-916c-75e23f0bbe80'),
|
||||||
|
(3, '8d995e32-56da-493f-9fdf-f7adba543e68', '7b861cf5-ffe7-4a42-9323-104bbf257aa1'),
|
||||||
|
(4, '4ad135b4-6f85-463d-86d5-4bd5347b5122', '41f13636-53e0-48de-9585-a80e8194e846'),
|
||||||
|
(5, '9ca415b2-a22d-4886-a0ff-d23a16c493a5', '7b861cf5-ffe7-4a42-9323-104bbf257aa1'),
|
||||||
|
(6, '5fe0d3b3-243c-4323-996e-856122dd1483', '7b861cf5-ffe7-4a42-9323-104bbf257aa1'),
|
||||||
|
(7, 'd98a42fe-69e6-4e85-83eb-2ed9bec79462', 'b4c116a4-6201-4e00-9b55-2bfe77ac262f'),
|
||||||
|
(8, 'ddcef114-7ffc-4d5b-88d8-9229904ce2f3', 'a6b02554-ab33-4e9d-916c-75e23f0bbe80'),
|
||||||
|
(9, '46f428fc-03cf-4977-ab56-382132c17c66', 'b4c116a4-6201-4e00-9b55-2bfe77ac262f'),
|
||||||
|
(10, '2f34b2f0-79ef-49de-b92b-e7b3b181a513', 'a6b02554-ab33-4e9d-916c-75e23f0bbe80'),
|
||||||
|
(11, '0c244c0f-4f04-41cd-958d-c53f1dbfaa8d', 'a6b02554-ab33-4e9d-916c-75e23f0bbe80');
|
||||||
|
|
||||||
|
INSERT INTO public.data_pegawai
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
sort,
|
||||||
|
user_created,
|
||||||
|
date_created,
|
||||||
|
user_updated,
|
||||||
|
date_updated,
|
||||||
|
"Foto_pegawai",
|
||||||
|
"Kartu_pegawai",
|
||||||
|
"KTP",
|
||||||
|
"Nomor_kartu_pegawai",
|
||||||
|
"NIP",
|
||||||
|
"NIP_lama",
|
||||||
|
"Jabatan",
|
||||||
|
"Golongan",
|
||||||
|
"Rumpun",
|
||||||
|
"KSM",
|
||||||
|
"Tanggal_masuk",
|
||||||
|
"Tanggal_pensiun",
|
||||||
|
"TMT_CPNS",
|
||||||
|
"TMT_PNS",
|
||||||
|
"Satuan_kerja",
|
||||||
|
"FK_spt_pegawai_id",
|
||||||
|
"Nama_depan",
|
||||||
|
"Nama_belakang",
|
||||||
|
"Username",
|
||||||
|
"User_email",
|
||||||
|
"Id_user",
|
||||||
|
"Practicioner",
|
||||||
|
"HFIS_code",
|
||||||
|
"HFIS_display",
|
||||||
|
"Code_dpjp",
|
||||||
|
"KDDOKTER",
|
||||||
|
"Practicioner_detail",
|
||||||
|
"Kode_satusehat",
|
||||||
|
"No_whatsapp",
|
||||||
|
"Posisi_ruang",
|
||||||
|
"Substansi",
|
||||||
|
"NDE",
|
||||||
|
"Subspesialis",
|
||||||
|
"Status_pegawai",
|
||||||
|
"File_status",
|
||||||
|
"Ketarangan",
|
||||||
|
"Kode_DPJP"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('7b861cf5-ffe7-4a42-9323-104bbf257aa1','draft',NULL,NULL,'2026-01-23 08:20:15.406222+07',NULL,NULL,NULL,NULL,NULL,NULL,'198001012010011001',NULL,NULL,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL,NULL,'Dr. Andi','Setiawan',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,1,NULL,NULL,NULL),
|
||||||
|
('b4c116a4-6201-4e00-9b55-2bfe77ac262f','draft',NULL,NULL,'2026-01-23 08:20:15.406222+07',NULL,NULL,NULL,NULL,NULL,NULL,'198205052010011002',NULL,NULL,NULL,NULL,1,NULL,NULL,NULL,NULL,NULL,NULL,'Dr. Budi','Santoso',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,2,1,NULL,NULL,NULL),
|
||||||
|
('a6b02554-ab33-4e9d-916c-75e23f0bbe80','draft',NULL,NULL,'2026-01-23 08:20:15.406222+07',NULL,NULL,NULL,NULL,NULL,NULL,'198503032012012003',NULL,NULL,NULL,NULL,2,NULL,NULL,NULL,NULL,NULL,NULL,'Dr. Citra','Lestari',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,3,1,NULL,NULL,NULL),
|
||||||
|
('41f13636-53e0-48de-9585-a80e8194e846','draft',NULL,NULL,'2026-01-23 08:20:15.406222+07',NULL,NULL,NULL,NULL,NULL,NULL,'198807072015012004',NULL,NULL,NULL,NULL,2,NULL,NULL,NULL,NULL,NULL,NULL,'Dr. Dewi','Anggraini',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,4,1,NULL,NULL,NULL),
|
||||||
|
('1338b3ff-5efa-4fe0-b070-485c818b7551','draft',NULL,NULL,'2026-01-23 08:20:15.406222+07',NULL,NULL,NULL,NULL,NULL,NULL,'199009092018011005',NULL,NULL,NULL,NULL,3,NULL,NULL,NULL,NULL,NULL,NULL,'Dr. Eko','Prasetio',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,5,1,NULL,NULL,NULL);
|
||||||
|
|
||||||
|
INSERT INTO public.data_telepon_pasien_operasi
|
||||||
|
(id, "Nomor_telepon", "FK_pasien_operasi_telepon_pasien_operasi_ID")
|
||||||
|
VALUES
|
||||||
|
('0df2c6e1-5ea2-4b47-a38d-acbc4e87acf3', '08594929242', '54b80a14-e557-482a-9c02-cf43312fe2b0'),
|
||||||
|
('87f8e21f-6332-4c65-a681-356acaaee379', '08840004491', '441d2ffb-7167-431c-aed2-c28069c88e56'),
|
||||||
|
('a42dabda-3a64-4bda-a3d2-619944ce93c0', '08644234690', '8d995e32-56da-493f-9fdf-f7adba543e68'),
|
||||||
|
('b4df90e4-0048-4944-8852-20bfcc9192ce', '08671298031', '4ad135b4-6f85-463d-86d5-4bd5347b5122'),
|
||||||
|
('c0f97b1e-2047-4b18-bb74-afdd32fb82bf', '08239962618', '9ca415b2-a22d-4886-a0ff-d23a16c493a5'),
|
||||||
|
('127b6583-904c-44cc-aae0-4f3877c95349', '08112641507', '5fe0d3b3-243c-4323-996e-856122dd1483'),
|
||||||
|
('0d71b0b8-7aa9-4542-8fd0-4e06c42b5379', '08836247929', 'd98a42fe-69e6-4e85-83eb-2ed9bec79462'),
|
||||||
|
('0390102d-95de-489b-a3f5-7b72fb8c4c7a', '08777144416', 'ddcef114-7ffc-4d5b-88d8-9229904ce2f3'),
|
||||||
|
('2354bf8d-f349-47c0-8bf6-e5ffcd07aa3a', '08138458824', '46f428fc-03cf-4977-ab56-382132c17c66'),
|
||||||
|
('541cc463-7104-45bd-b3da-27db82cd6722', '08695133731', '2f34b2f0-79ef-49de-b92b-e7b3b181a513'),
|
||||||
|
('8519adf3-b728-4ca2-a7ae-fada50978a6d', '08515321902', '0c244c0f-4f04-41cd-958d-c53f1dbfaa8d');
|
||||||
|
|
||||||
|
INSERT INTO public.data_tindakan_pasien_operasi
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
user_created,
|
||||||
|
date_created,
|
||||||
|
user_updated,
|
||||||
|
date_updated,
|
||||||
|
"Kode_tindakan",
|
||||||
|
"Tindakan",
|
||||||
|
"Tindakan_tambahan",
|
||||||
|
"FK_pasien_operasi_tindakan_pasien_operasi_ID"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('e1c9cb34-b65d-4eda-9b5d-acc18375e095', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-12', 'Hemorrhoidectomy', NULL, '54b80a14-e557-482a-9c02-cf43312fe2b0'),
|
||||||
|
('14b24c41-440a-4c7e-8ca7-6578777c51eb', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-28', 'Hemorrhoidectomy', NULL, '441d2ffb-7167-431c-aed2-c28069c88e56'),
|
||||||
|
('441455cc-9e97-4b34-82eb-076f8e9dbcbc', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-5', 'Mastectomy', NULL, '8d995e32-56da-493f-9fdf-f7adba543e68'),
|
||||||
|
('e70b1de6-19dc-4e96-9d25-016346b460d2', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-93', 'Tonsillectomy', NULL, '4ad135b4-6f85-463d-86d5-4bd5347b5122'),
|
||||||
|
('afb5a9a6-2d3f-48f0-b3e5-110db806a77f', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-84', 'Phacoemulsification', NULL, '9ca415b2-a22d-4886-a0ff-d23a16c493a5'),
|
||||||
|
('d70d31e4-add4-4bd9-91b2-9e349f117379', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-58', 'Herniotomy', NULL, '5fe0d3b3-243c-4323-996e-856122dd1483'),
|
||||||
|
('473c3c5a-00f0-499e-95e5-aa622e160eaf', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-42', 'ORIF', NULL, 'd98a42fe-69e6-4e85-83eb-2ed9bec79462'),
|
||||||
|
('2e666e5a-b10d-4077-b5a1-3d6a6942a11c', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-80', 'Phacoemulsification', NULL, 'ddcef114-7ffc-4d5b-88d8-9229904ce2f3'),
|
||||||
|
('62a5b0ec-8a96-4111-a562-1fbea465b3fa', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-93', 'Sectio Caesarea', NULL, '46f428fc-03cf-4977-ab56-382132c17c66'),
|
||||||
|
('6460fe0b-85d6-4895-84b8-3a71fb889138', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-12', 'Herniotomy', NULL, '2f34b2f0-79ef-49de-b92b-e7b3b181a513'),
|
||||||
|
('e072a775-13bb-4a0d-b946-8b64c463020d', NULL, '2026-01-23 08:22:48.003155+07', NULL, NULL, 'ICD-9-25', 'ORIF', NULL, '0c244c0f-4f04-41cd-958d-c53f1dbfaa8d');
|
||||||
Reference in New Issue
Block a user