harryagasi
[KM-418]feat: align analysis flow contracts and scoped catalog rebuild
80fa986
Raw
History Blame Contribute Delete
11 kB
package database_clients
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"project/internal/api"
"project/internal/auth"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
// ListDBTypes godoc
//
// @Summary List tipe database yang didukung beserta definisi field kredensial
// @Description Mengembalikan semua tipe database beserta schema field yang dibutuhkan untuk membuat koneksi. Gunakan untuk render form dinamis di frontend.
// @Tags database-clients
// @Produce json
// @Success 200 {object} api.APIResponse{data=[]DBTypeInfo}
// @Security BearerAuth
// @Router /api/v1/database-clients/dbtypes [get]
func (h *Handler) ListDBTypes(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, api.APIResponse{
Status: "success",
Message: "supported database types",
Data: h.svc.ListDBTypes(),
})
}
// Create godoc
//
// @Summary Daftarkan koneksi database baru
// @Description Enkripsi credentials dan simpan. Jika sudah ada koneksi dengan identity yang sama, kembalikan yang existing (HTTP 200).
// @Tags database-clients
// @Accept json
// @Produce json
// @Param body body object{user_id=string,name=string,db_type=string,credentials=object} true "Database connection info"
// @Success 201 {object} api.APIResponse{data=DBClient}
// @Success 200 {object} api.APIResponse{data=DBClient} "Duplicate — returning existing"
// @Failure 400 {object} api.APIResponse
// @Failure 429 {object} map[string]string "Rate limit exceeded"
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients [post]
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
var req struct {
UserID string `json:"user_id"`
Name string `json:"name"`
DBType string `json:"db_type"`
Credentials map[string]any `json:"credentials"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "invalid request body"})
return
}
if req.UserID == "" || req.Name == "" || req.DBType == "" || req.Credentials == nil {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id, name, db_type, and credentials are required"})
return
}
if rejectUserMismatch(w, r, req.UserID) {
return
}
client, isDuplicate, err := h.svc.Create(r.Context(), req.UserID, req.Name, req.DBType, req.Credentials)
if err != nil {
if errors.Is(err, ErrUnsupportedType) {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "unsupported database type"})
return
}
slog.Error("database client create failed", "err", err)
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"})
return
}
status := http.StatusCreated
msg := "database client created"
if isDuplicate {
status = http.StatusOK
msg = "database client already exists"
}
writeJSON(w, status, api.APIResponse{Status: "success", Message: msg, Data: client})
}
// List godoc
//
// @Summary List koneksi database milik user
// @Tags database-clients
// @Produce json
// @Param user_id path string true "User ID"
// @Success 200 {object} api.APIResponse{data=[]DBClient}
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients/{user_id} [get]
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("user_id")
if rejectUserMismatch(w, r, userID) {
return
}
clients, err := h.svc.List(r.Context(), userID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"})
return
}
writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database clients", Data: clients})
}
// GetOne godoc
//
// @Summary Ambil detail satu koneksi database
// @Tags database-clients
// @Produce json
// @Param user_id path string true "User ID"
// @Param client_id path string true "Client ID"
// @Success 200 {object} api.APIResponse{data=DBClient}
// @Failure 403 {object} api.APIResponse
// @Failure 404 {object} api.APIResponse
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients/{user_id}/{client_id} [get]
func (h *Handler) GetOne(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("user_id")
if rejectUserMismatch(w, r, userID) {
return
}
clientID := r.PathValue("client_id")
client, err := h.svc.Get(r.Context(), userID, clientID)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"})
case errors.Is(err, ErrForbidden):
writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"})
default:
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"})
}
return
}
writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client", Data: client})
}
// Update godoc
//
// @Summary Update koneksi database
// @Tags database-clients
// @Accept json
// @Produce json
// @Param client_id path string true "Client ID"
// @Param user_id query string true "User ID (ownership check)"
// @Param body body object{name=string,credentials=object,status=string} false "Fields to update"
// @Success 200 {object} api.APIResponse{data=DBClient}
// @Failure 400 {object} api.APIResponse
// @Failure 403 {object} api.APIResponse
// @Failure 404 {object} api.APIResponse
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients/{client_id} [put]
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
clientID := r.PathValue("client_id")
userID := r.URL.Query().Get("user_id")
if userID == "" {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"})
return
}
if rejectUserMismatch(w, r, userID) {
return
}
var req struct {
Name *string `json:"name"`
Credentials map[string]any `json:"credentials"`
Status *string `json:"status"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "invalid request body"})
return
}
client, err := h.svc.Update(r.Context(), clientID, userID, req.Name, req.Credentials, req.Status)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"})
case errors.Is(err, ErrForbidden):
writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"})
default:
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"})
}
return
}
writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client updated", Data: client})
}
// Delete godoc
//
// @Summary Hapus koneksi database
// @Description Menghapus koneksi dan memicu pembersihan catalog
// @Tags database-clients
// @Produce json
// @Param client_id path string true "Client ID"
// @Param user_id query string true "User ID (ownership check)"
// @Success 200 {object} api.APIResponse
// @Failure 400 {object} api.APIResponse
// @Failure 403 {object} api.APIResponse
// @Failure 404 {object} api.APIResponse
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients/{client_id} [delete]
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
clientID := r.PathValue("client_id")
userID := r.URL.Query().Get("user_id")
if userID == "" {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"})
return
}
if rejectUserMismatch(w, r, userID) {
return
}
if err := h.svc.Delete(r.Context(), clientID, userID); err != nil {
switch {
case errors.Is(err, ErrNotFound):
writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"})
case errors.Is(err, ErrForbidden):
writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"})
default:
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"})
}
return
}
writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client deleted"})
}
// Ingest godoc
//
// @Summary Introspect schema database dan simpan ke catalog
// @Tags database-clients
// @Produce json
// @Param client_id path string true "Client ID"
// @Param user_id query string true "User ID (ownership check)"
// @Success 200 {object} api.APIResponse
// @Failure 400 {object} api.APIResponse
// @Failure 403 {object} api.APIResponse
// @Failure 404 {object} api.APIResponse
// @Failure 409 {object} api.APIResponse "Client is inactive"
// @Failure 429 {object} map[string]string "Rate limit exceeded"
// @Failure 500 {object} api.APIResponse
// @Security BearerAuth
// @Router /api/v1/database-clients/{client_id}/ingest [post]
func (h *Handler) Ingest(w http.ResponseWriter, r *http.Request) {
clientID := r.PathValue("client_id")
userID := r.URL.Query().Get("user_id")
if userID == "" {
writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"})
return
}
if rejectUserMismatch(w, r, userID) {
return
}
schema, err := h.svc.Ingest(r.Context(), clientID, userID)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"})
case errors.Is(err, ErrForbidden):
writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"})
case errors.Is(err, ErrInactive):
writeJSON(w, http.StatusConflict, api.APIResponse{Status: "error", Message: "database client is inactive"})
default:
writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: err.Error()})
}
return
}
writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "schema ingested", Data: schema})
}
func rejectUserMismatch(w http.ResponseWriter, r *http.Request, userID string) bool {
if err := auth.MatchContextUserID(r.Context(), userID); err != nil {
writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "user_id does not match authenticated user"})
return true
}
return false
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}