forked from rachmadiyanti.annisa.3004/service_antrean
Intial commit
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ import (
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @host localhost:8084
|
||||
// @BasePath /api/v1
|
||||
// @schemes http https
|
||||
|
||||
|
||||
+2574
-1
File diff suppressed because it is too large
Load Diff
+2574
-1
File diff suppressed because it is too large
Load Diff
+1728
-1
File diff suppressed because it is too large
Load Diff
@@ -197,7 +197,7 @@ func LoadConfig() *Config {
|
||||
log.Printf("DEBUG: Raw ENV for SECURITY_MAX_INPUT_LENGTH is: '%s'", os.Getenv("SECURITY_MAX_INPUT_LENGTH"))
|
||||
config := &Config{
|
||||
Server: ServerConfig{
|
||||
Port: getEnvAsInt("PORT", 8080),
|
||||
Port: getEnvAsInt("PORT", 8084),
|
||||
Mode: getEnv("GIN_MODE", "debug"),
|
||||
},
|
||||
Databases: make(map[string]DatabaseConfig),
|
||||
@@ -232,12 +232,12 @@ func LoadConfig() *Config {
|
||||
ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"),
|
||||
LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"),
|
||||
LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"),
|
||||
Host: getEnv("SWAGGER_HOST", "localhost:8080"),
|
||||
Host: getEnv("SWAGGER_HOST", "localhost:8084"),
|
||||
BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"),
|
||||
Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")),
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost:8080")),
|
||||
TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost:8084")),
|
||||
MaxInputLength: getEnvAsInt("SECURITY_MAX_INPUT_LENGTH", 500),
|
||||
RateLimit: RateLimitConfig{
|
||||
RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
|
||||
@@ -1195,7 +1195,7 @@ func parseStaticTokens(tokensStr string) []string {
|
||||
|
||||
func parseOrigins(originsStr string) []string {
|
||||
if originsStr == "" {
|
||||
return []string{"http://localhost:8080"} // Default untuk pengembangan
|
||||
return []string{"http://localhost:8084"} // Default untuk pengembangan
|
||||
}
|
||||
origins := strings.Split(originsStr, ",")
|
||||
for i, origin := range origins {
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
models "api-service/internal/models"
|
||||
componentModels "api-service/internal/models/component"
|
||||
queryUtils "api-service/internal/utils/query"
|
||||
"api-service/internal/utils/validation"
|
||||
"api-service/pkg/logger"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// GLOBAL INITIALIZATION & VALIDATION
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
db database.Service
|
||||
once sync.Once
|
||||
validate *validator.Validate
|
||||
)
|
||||
|
||||
// Initialize the database connection and validator once
|
||||
func init() {
|
||||
once.Do(func() {
|
||||
db = database.New(config.LoadConfig())
|
||||
validate = validator.New()
|
||||
validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus)
|
||||
if db == nil {
|
||||
logger.Fatal("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Custom validation for rol_pages status
|
||||
func validateRol_pagesStatus(fl validator.FieldLevel) bool {
|
||||
return models.IsValidStatus(fl.Field().String())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CACHE IMPLEMENTATION
|
||||
// =============================================================================
|
||||
|
||||
// CacheEntry represents an entry in the cache
|
||||
type CacheEntry struct {
|
||||
Data interface{}
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// IsExpired checks if the cache entry has expired
|
||||
func (e *CacheEntry) IsExpired() bool {
|
||||
return time.Now().After(e.ExpiresAt)
|
||||
}
|
||||
|
||||
// InMemoryCache implements a simple in-memory cache with TTL
|
||||
type InMemoryCache struct {
|
||||
items sync.Map
|
||||
}
|
||||
|
||||
// NewInMemoryCache creates a new in-memory cache
|
||||
func NewInMemoryCache() *InMemoryCache {
|
||||
return &InMemoryCache{}
|
||||
}
|
||||
|
||||
// Get retrieves an item from the cache
|
||||
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
|
||||
val, ok := c.items.Load(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
entry, ok := val.(*CacheEntry)
|
||||
if !ok || entry.IsExpired() {
|
||||
c.items.Delete(key)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.Data, true
|
||||
}
|
||||
|
||||
// Set stores an item in the cache with a TTL
|
||||
func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) {
|
||||
entry := &CacheEntry{
|
||||
Data: value,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
c.items.Store(key, entry)
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache
|
||||
func (c *InMemoryCache) Delete(key string) {
|
||||
c.items.Delete(key)
|
||||
}
|
||||
|
||||
// DeleteByPrefix removes all items with a specific prefix
|
||||
func (c *InMemoryCache) DeleteByPrefix(prefix string) {
|
||||
c.items.Range(func(key, value interface{}) bool {
|
||||
if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix {
|
||||
c.items.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ROL_COMPONENT HANDLER STRUCT
|
||||
// =============================================================================
|
||||
|
||||
// Rol_componentHandler handles rol_component services
|
||||
type Rol_componentHandler struct {
|
||||
db database.Service
|
||||
queryBuilder *queryUtils.QueryBuilder
|
||||
validator *validation.DynamicValidator
|
||||
cache *InMemoryCache // Re-using the same cache type
|
||||
}
|
||||
|
||||
// NewRol_componentHandler creates a new Rol_componentHandler
|
||||
func NewRol_componentHandler() *Rol_componentHandler {
|
||||
// Initialize QueryBuilder with allowed columns for security.
|
||||
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
|
||||
SetAllowedColumns([]string{
|
||||
"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id",
|
||||
})
|
||||
|
||||
return &Rol_componentHandler{
|
||||
// We reuse the global 'db' instance from the init() function in rol_pages_handler.go
|
||||
db: db,
|
||||
queryBuilder: queryBuilder,
|
||||
validator: validation.NewDynamicValidator(queryBuilder),
|
||||
cache: NewInMemoryCache(), // Each handler gets its own cache instance
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HANDLER ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
// GetRol_components godoc
|
||||
// @Summary Get Components List
|
||||
// @Description Get list of components with pagination and filters
|
||||
// @Tags Components
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit (max 100)" default(10)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Param page_id query int false "Filter by parent page ID"
|
||||
// @Param active query string false "Filter by status"
|
||||
// @Success 200 {object} componentModels.ComponentsGetResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /component [get]
|
||||
func (h *Rol_componentHandler) GetRol_components(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := queryUtils.DynamicQuery{
|
||||
From: "role_access.rol_component",
|
||||
Aliases: "rc",
|
||||
Fields: []queryUtils.SelectField{
|
||||
{Expression: "rc.id", Alias: "id"},
|
||||
{Expression: "rc.name", Alias: "name"},
|
||||
{Expression: "rc.description", Alias: "description"},
|
||||
{Expression: "rc.directory", Alias: "directory"},
|
||||
{Expression: "rc.active", Alias: "active"},
|
||||
{Expression: "rc.sort", Alias: "sort"},
|
||||
{Expression: "rc.fk_rol_pages_id", Alias: "fk_rol_pages_id"},
|
||||
},
|
||||
Sort: []queryUtils.SortField{
|
||||
{Column: "rc.fk_rol_pages_id", Order: "ASC"},
|
||||
{Column: "rc.sort", Order: "ASC"},
|
||||
},
|
||||
}
|
||||
|
||||
// Parse pagination
|
||||
if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 {
|
||||
query.Limit = limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 {
|
||||
query.Offset = offset
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse filters
|
||||
var filters []queryUtils.DynamicFilter
|
||||
if pageID := c.Query("page_id"); pageID != "" {
|
||||
if id, err := strconv.ParseInt(pageID, 10, 64); err == nil {
|
||||
filters = append(filters, queryUtils.DynamicFilter{Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id})
|
||||
}
|
||||
}
|
||||
if active := c.Query("active"); active != "" && models.IsValidStatus(active) {
|
||||
filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active})
|
||||
}
|
||||
|
||||
if len(filters) > 0 {
|
||||
query.Filters = []queryUtils.FilterGroup{{Filters: filters, LogicOp: "AND"}}
|
||||
}
|
||||
|
||||
// Execute query
|
||||
var components []componentModels.Rol_component
|
||||
err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &components)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
total, err := h.getTotalCount(ctx, dbConn, query)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
meta := h.calculateMeta(query.Limit, query.Offset, total)
|
||||
response := componentModels.ComponentsGetResponse{
|
||||
Message: "Data rol_component berhasil diambil",
|
||||
Data: components,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateRol_component godoc
|
||||
// @Summary Create Component
|
||||
// @Description Create a new component
|
||||
// @Tags Components
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body componentModels.ComponentCreateRequest true "Component creation request"
|
||||
// @Success 201 {object} componentModels.ComponentCreateResponse "Component created successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /component [post]
|
||||
func (h *Rol_componentHandler) CreateRol_component(c *gin.Context) {
|
||||
var req componentModels.ComponentCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data := queryUtils.InsertData{
|
||||
Columns: []string{"name", "description", "directory", "active", "sort", "fk_rol_pages_id"},
|
||||
Values: []interface{}{req.Name, req.Description, req.Directory, req.Active, req.Sort, req.FkRolPagesID},
|
||||
}
|
||||
returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"}
|
||||
|
||||
sql, args, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", data, returningCols...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var dataComponent componentModels.Rol_component
|
||||
err = dbConn.GetContext(ctx, &dataComponent, sql, args...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to create rol_component", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := componentModels.ComponentCreateResponse{Message: "Rol_component berhasil dibuat", Data: &dataComponent}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateRol_component godoc
|
||||
// @Summary Update Component
|
||||
// @Description Update an existing component
|
||||
// @Tags Components
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Component ID"
|
||||
// @Param request body componentModels.ComponentUpdateRequest true "Component update request"
|
||||
// @Success 200 {object} componentModels.ComponentUpdateResponse "Component updated successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 404 {object} models.ErrorResponse "Component not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /component/{id} [put]
|
||||
func (h *Rol_componentHandler) UpdateRol_component(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req componentModels.ComponentUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req.ID = &idInt
|
||||
if err := validate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
columns := make([]string, 0, 6)
|
||||
values := make([]interface{}, 0, 6)
|
||||
|
||||
if req.Name != nil {
|
||||
columns = append(columns, "name")
|
||||
values = append(values, *req.Name)
|
||||
}
|
||||
if req.Description != nil {
|
||||
columns = append(columns, "description")
|
||||
values = append(values, *req.Description) // keep as string; if you want SQL NULL for empty use sql.NullString
|
||||
}
|
||||
if req.Directory != nil {
|
||||
columns = append(columns, "directory")
|
||||
values = append(values, *req.Directory)
|
||||
}
|
||||
if req.Active != nil {
|
||||
columns = append(columns, "active")
|
||||
values = append(values, *req.Active)
|
||||
}
|
||||
if req.Sort != nil {
|
||||
columns = append(columns, "sort")
|
||||
values = append(values, *req.Sort)
|
||||
}
|
||||
if req.FkRolPagesID != nil {
|
||||
columns = append(columns, "fk_rol_pages_id")
|
||||
values = append(values, *req.FkRolPagesID)
|
||||
}
|
||||
|
||||
if len(columns) == 0 {
|
||||
h.respondError(c, "No fields to update", fmt.Errorf("empty update payload"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
updateData := queryUtils.UpdateData{
|
||||
Columns: columns,
|
||||
Values: values,
|
||||
}
|
||||
filters := []queryUtils.FilterGroup{{
|
||||
Filters: []queryUtils.DynamicFilter{
|
||||
{Column: "id", Operator: queryUtils.OpEqual, Value: req.ID},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
}}
|
||||
returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"}
|
||||
|
||||
sql, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", updateData, filters, returningCols...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var dataComponent componentModels.Rol_component
|
||||
err = dbConn.GetContext(ctx, &dataComponent, sql, args...)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
h.respondError(c, "Rol_component not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to update rol_component", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := componentModels.ComponentUpdateResponse{
|
||||
Message: "Rol_component berhasil diperbarui",
|
||||
Data: &dataComponent,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteRol_component godoc
|
||||
// @Summary Delete Component
|
||||
// @Description Delete a component
|
||||
// @Tags Components
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Component ID"
|
||||
// @Success 200 {object} componentModels.ComponentDeleteResponse "Component deleted successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Component not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /component/{id} [delete]
|
||||
func (h *Rol_componentHandler) DeleteRol_component(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Soft delete by changing status
|
||||
updateData := queryUtils.UpdateData{
|
||||
Columns: []string{"active"},
|
||||
Values: []interface{}{false},
|
||||
}
|
||||
filters := []queryUtils.FilterGroup{{
|
||||
Filters: []queryUtils.DynamicFilter{
|
||||
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
|
||||
{Column: "active", Operator: queryUtils.OpNotEqual, Value: false},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
}}
|
||||
|
||||
result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "role_access.rol_component", updateData, filters)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to delete rol_component", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
h.respondError(c, "Rol_component not found", sql.ErrNoRows, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := componentModels.ComponentDeleteResponse{Message: "Rol_component berhasil dihapus", ID: id}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// invalidateRelatedCache clears cache entries that might be affected by component changes.
|
||||
// This includes the pages cache, since pages contain components.
|
||||
func (h *Rol_componentHandler) invalidateRelatedCache() {
|
||||
h.cache.DeleteByPrefix("rol_component:")
|
||||
h.cache.DeleteByPrefix("rol_pages:") // Invalidate pages cache as well
|
||||
}
|
||||
|
||||
func (h *Rol_componentHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) {
|
||||
countQuery := queryUtils.DynamicQuery{
|
||||
From: query.From,
|
||||
Aliases: query.Aliases,
|
||||
Filters: query.Filters,
|
||||
}
|
||||
|
||||
count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to execute count query: %w", err)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
func (h *Rol_componentHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode})
|
||||
h.respondError(c, message, err, statusCode)
|
||||
}
|
||||
|
||||
func (h *Rol_componentHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
errorMessage := message
|
||||
if gin.Mode() == gin.ReleaseMode {
|
||||
errorMessage = "Internal server error"
|
||||
}
|
||||
c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()})
|
||||
}
|
||||
|
||||
func (h *Rol_componentHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||
totalPages, currentPage := 0, 1
|
||||
if limit > 0 {
|
||||
totalPages = (total + limit - 1) / limit
|
||||
currentPage = (offset / limit) + 1
|
||||
}
|
||||
return models.MetaResponse{
|
||||
Limit: limit, Offset: offset, Total: total, TotalPages: totalPages,
|
||||
CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,487 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
models "api-service/internal/models"
|
||||
permissionModels "api-service/internal/models/permission"
|
||||
queryUtils "api-service/internal/utils/query"
|
||||
"api-service/internal/utils/validation"
|
||||
"api-service/pkg/logger"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// GLOBAL INITIALIZATION & VALIDATION
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
db database.Service
|
||||
once sync.Once
|
||||
validate *validator.Validate
|
||||
)
|
||||
|
||||
// Initialize the database connection and validator once
|
||||
func init() {
|
||||
once.Do(func() {
|
||||
db = database.New(config.LoadConfig())
|
||||
validate = validator.New()
|
||||
validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus)
|
||||
if db == nil {
|
||||
logger.Fatal("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Custom validation for rol_pages status
|
||||
func validateRol_pagesStatus(fl validator.FieldLevel) bool {
|
||||
return models.IsValidStatus(fl.Field().String())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CACHE IMPLEMENTATION
|
||||
// =============================================================================
|
||||
|
||||
// CacheEntry represents an entry in the cache
|
||||
type CacheEntry struct {
|
||||
Data interface{}
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// IsExpired checks if the cache entry has expired
|
||||
func (e *CacheEntry) IsExpired() bool {
|
||||
return time.Now().After(e.ExpiresAt)
|
||||
}
|
||||
|
||||
// InMemoryCache implements a simple in-memory cache with TTL
|
||||
type InMemoryCache struct {
|
||||
items sync.Map
|
||||
}
|
||||
|
||||
// NewInMemoryCache creates a new in-memory cache
|
||||
func NewInMemoryCache() *InMemoryCache {
|
||||
return &InMemoryCache{}
|
||||
}
|
||||
|
||||
// Get retrieves an item from the cache
|
||||
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
|
||||
val, ok := c.items.Load(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
entry, ok := val.(*CacheEntry)
|
||||
if !ok || entry.IsExpired() {
|
||||
c.items.Delete(key)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.Data, true
|
||||
}
|
||||
|
||||
// Set stores an item in the cache with a TTL
|
||||
func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) {
|
||||
entry := &CacheEntry{
|
||||
Data: value,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
c.items.Store(key, entry)
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache
|
||||
func (c *InMemoryCache) Delete(key string) {
|
||||
c.items.Delete(key)
|
||||
}
|
||||
|
||||
// DeleteByPrefix removes all items with a specific prefix
|
||||
func (c *InMemoryCache) DeleteByPrefix(prefix string) {
|
||||
c.items.Range(func(key, value interface{}) bool {
|
||||
if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix {
|
||||
c.items.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ROL_PERMISSION HANDLER STRUCT
|
||||
// =============================================================================
|
||||
|
||||
// Rol_permissionHandler handles rol_permission services
|
||||
type Rol_permissionHandler struct {
|
||||
db database.Service
|
||||
queryBuilder *queryUtils.QueryBuilder
|
||||
validator *validation.DynamicValidator
|
||||
cache *InMemoryCache
|
||||
}
|
||||
|
||||
// NewRol_permissionHandler creates a new Rol_permissionHandler
|
||||
func NewRol_permissionHandler() *Rol_permissionHandler {
|
||||
// Initialize QueryBuilder with allowed columns for security.
|
||||
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
|
||||
SetAllowedColumns([]string{
|
||||
"id", "create", "read", "update", "disable", "delete", "active",
|
||||
"fk_rol_pages_id", "role_keycloak", "group_keycloak",
|
||||
})
|
||||
|
||||
return &Rol_permissionHandler{
|
||||
db: db,
|
||||
queryBuilder: queryBuilder,
|
||||
validator: validation.NewDynamicValidator(queryBuilder),
|
||||
cache: NewInMemoryCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HANDLER ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
// GetRol_permissions godoc
|
||||
// @Summary Get Permissions List
|
||||
// @Description Get list of permissions with pagination and filters
|
||||
// @Tags Permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit (max 100)" default(10)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Param page_id query int false "Filter by parent page ID"
|
||||
// @Param active query string false "Filter by status"
|
||||
// @Success 200 {object} permissionModels.PermissionsGetResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /permission [get]
|
||||
func (h *Rol_permissionHandler) GetRol_permissions(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := queryUtils.DynamicQuery{
|
||||
From: "role_access.rol_permission",
|
||||
Aliases: "rper",
|
||||
Fields: []queryUtils.SelectField{
|
||||
{Expression: "rper.id", Alias: "id"},
|
||||
{Expression: "rper.create", Alias: "create"},
|
||||
{Expression: "rper.read", Alias: "read"},
|
||||
{Expression: "rper.update", Alias: "update"},
|
||||
{Expression: "rper.disable", Alias: "disable"},
|
||||
{Expression: "rper.delete", Alias: "delete"},
|
||||
{Expression: "rper.active", Alias: "active"},
|
||||
{Expression: "rper.fk_rol_pages_id", Alias: "fk_rol_pages_id"},
|
||||
{Expression: "rper.role_keycloak", Alias: "role_keycloak"},
|
||||
{Expression: "rper.group_keycloak", Alias: "group_keycloak"},
|
||||
},
|
||||
Sort: []queryUtils.SortField{
|
||||
{Column: "rper.fk_rol_pages_id", Order: "ASC"},
|
||||
{Column: "rper.id", Order: "ASC"},
|
||||
},
|
||||
}
|
||||
|
||||
// Parse pagination
|
||||
if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 {
|
||||
query.Limit = limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 {
|
||||
query.Offset = offset
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse filters
|
||||
var filters []queryUtils.DynamicFilter
|
||||
if pageID := c.Query("page_id"); pageID != "" {
|
||||
if id, err := strconv.ParseInt(pageID, 10, 64); err == nil {
|
||||
filters = append(filters, queryUtils.DynamicFilter{Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id})
|
||||
}
|
||||
}
|
||||
if active := c.Query("active"); active != "" && models.IsValidStatus(active) {
|
||||
filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active})
|
||||
}
|
||||
|
||||
if len(filters) > 0 {
|
||||
query.Filters = []queryUtils.FilterGroup{{Filters: filters, LogicOp: "AND"}}
|
||||
}
|
||||
|
||||
// Execute query
|
||||
var permissions []permissionModels.Rol_permission
|
||||
err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &permissions)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
total, err := h.getTotalCount(ctx, dbConn, query)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
meta := h.calculateMeta(query.Limit, query.Offset, total)
|
||||
response := permissionModels.PermissionsGetResponse{
|
||||
Message: "Data rol_permission berhasil diambil",
|
||||
Data: permissions,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateRol_permission godoc
|
||||
// @Summary Create Permission
|
||||
// @Description Create a new permission
|
||||
// @Tags Permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body permissionModels.PermissionCreateRequest true "Permission creation request"
|
||||
// @Success 201 {object} permissionModels.PermissionCreateResponse "Permission created successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /permission [post]
|
||||
func (h *Rol_permissionHandler) CreateRol_permission(c *gin.Context) {
|
||||
var req permissionModels.PermissionCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data := queryUtils.InsertData{
|
||||
Columns: []string{"create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"},
|
||||
Values: []interface{}{req.Create, req.Read, req.Update, req.Disable, req.Delete, req.Active, req.FkRolPagesID, req.RoleKeycloak, req.GroupKeycloak},
|
||||
}
|
||||
returningCols := []string{"id", "create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"}
|
||||
|
||||
sql, args, err := h.queryBuilder.BuildInsertQuery("role_access.rol_permission", data, returningCols...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var dataPermission permissionModels.Rol_permission
|
||||
err = dbConn.GetContext(ctx, &dataPermission, sql, args...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to create rol_permission", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := permissionModels.PermissionCreateResponse{Message: "Rol_permission berhasil dibuat", Data: &dataPermission}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateRol_permission godoc
|
||||
// @Summary Update Permission
|
||||
// @Description Update an existing permission
|
||||
// @Tags Permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Permission ID"
|
||||
// @Param request body permissionModels.PermissionUpdateRequest true "Permission update request"
|
||||
// @Success 200 {object} permissionModels.PermissionUpdateResponse "Permission updated successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 404 {object} models.ErrorResponse "Permission not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /permission/{id} [put]
|
||||
func (h *Rol_permissionHandler) UpdateRol_permission(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req permissionModels.PermissionUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req.ID = &idInt
|
||||
if err := validate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
updateData := queryUtils.UpdateData{
|
||||
Columns: []string{"create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"},
|
||||
Values: []interface{}{req.Create, req.Read, req.Update, req.Disable, req.Delete, req.Active, req.FkRolPagesID, req.RoleKeycloak, req.GroupKeycloak},
|
||||
}
|
||||
filters := []queryUtils.FilterGroup{{
|
||||
Filters: []queryUtils.DynamicFilter{
|
||||
{Column: "id", Operator: queryUtils.OpEqual, Value: req.ID},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
}}
|
||||
returningCols := []string{"id", "create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"}
|
||||
|
||||
sql, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", updateData, filters, returningCols...)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var dataPermission permissionModels.Rol_permission
|
||||
err = dbConn.GetContext(ctx, &dataPermission, sql, args...)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
h.respondError(c, "Rol_permission not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to update rol_permission", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := permissionModels.PermissionUpdateResponse{Message: "Rol_permission berhasil diperbarui", Data: &dataPermission}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteRol_permission godoc
|
||||
// @Summary Delete Permission
|
||||
// @Description Delete a permission
|
||||
// @Tags Permissions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Permission ID"
|
||||
// @Success 200 {object} permissionModels.PermissionDeleteResponse "Permission deleted successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Permission not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /permission/{id} [delete]
|
||||
func (h *Rol_permissionHandler) DeleteRol_permission(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetSQLXDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Soft delete by changing status
|
||||
updateData := queryUtils.UpdateData{
|
||||
Columns: []string{"active"},
|
||||
Values: []interface{}{false},
|
||||
}
|
||||
filters := []queryUtils.FilterGroup{{
|
||||
Filters: []queryUtils.DynamicFilter{
|
||||
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
|
||||
{Column: "active", Operator: queryUtils.OpNotEqual, Value: false},
|
||||
},
|
||||
LogicOp: "AND",
|
||||
}}
|
||||
|
||||
result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "role_access.rol_permission", updateData, filters)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to delete rol_permission", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
h.respondError(c, "Rol_permission not found", sql.ErrNoRows, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.invalidateRelatedCache()
|
||||
response := permissionModels.PermissionDeleteResponse{Message: "Rol_permission berhasil dihapus", ID: id}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// invalidateRelatedCache clears cache entries that might be affected by permission changes.
|
||||
// This includes pages cache, since pages contain permissions.
|
||||
func (h *Rol_permissionHandler) invalidateRelatedCache() {
|
||||
h.cache.DeleteByPrefix("rol_permission:")
|
||||
h.cache.DeleteByPrefix("rol_pages:") // Invalidate pages cache as well
|
||||
}
|
||||
|
||||
func (h *Rol_permissionHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) {
|
||||
countQuery := queryUtils.DynamicQuery{
|
||||
From: query.From,
|
||||
Aliases: query.Aliases,
|
||||
Filters: query.Filters,
|
||||
}
|
||||
|
||||
count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to execute count query: %w", err)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
func (h *Rol_permissionHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode})
|
||||
h.respondError(c, message, err, statusCode)
|
||||
}
|
||||
|
||||
func (h *Rol_permissionHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
errorMessage := message
|
||||
if gin.Mode() == gin.ReleaseMode {
|
||||
errorMessage = "Internal server error"
|
||||
}
|
||||
c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()})
|
||||
}
|
||||
|
||||
func (h *Rol_permissionHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||
totalPages, currentPage := 0, 1
|
||||
if limit > 0 {
|
||||
totalPages = (total + limit - 1) / limit
|
||||
currentPage = (offset / limit) + 1
|
||||
}
|
||||
return models.MetaResponse{
|
||||
Limit: limit, Offset: offset, Total: total, TotalPages: totalPages,
|
||||
CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Rol_component represents the data structure for the role_access.rol_component table
|
||||
// with proper null handling and optimized JSON marshaling.
|
||||
type Rol_component struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description sql.NullString `json:"description,omitempty" db:"description"`
|
||||
Directory string `json:"directory" db:"directory"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
FkRolPagesID int64 `json:"fk_rol_pages_id" db:"fk_rol_pages_id"`
|
||||
Sort int64 `json:"sort" db:"sort"`
|
||||
}
|
||||
|
||||
// MarshalJSON for Rol_component handles the sql.NullString field 'Description'
|
||||
// to ensure it appears as null, an empty string, or the string value in JSON.
|
||||
func (r Rol_component) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rol_component
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Description *string `json:"description,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Description.Valid {
|
||||
aux.Description = &r.Description.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper method to safely get Description
|
||||
func (r *Rol_component) GetDescription() string {
|
||||
if r.Description.Valid {
|
||||
return r.Description.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REQUEST & RESPONSE STRUCTS FOR ROL_COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
// ComponentCreateRequest defines the structure for creating a new component.
|
||||
type ComponentCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=100"`
|
||||
Description *string `json:"description" validate:"omitempty,max=255"`
|
||||
Directory string `json:"directory" validate:"required,min=1,max=255"`
|
||||
Active bool `json:"active"`
|
||||
Sort int64 `json:"sort"`
|
||||
FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// ComponentUpdateRequest defines the structure for updating an existing component.
|
||||
// ID is handled via URL parameter, so it's omitted from JSON with `json:"-"`.
|
||||
type ComponentUpdateRequest struct {
|
||||
ID *int `json:"-" validate:"required"` // ID is from URL param
|
||||
Name *string `json:"name" validate:"omitempty,min=1,max=100"`
|
||||
Description *string `json:"description" validate:"omitempty,max=255"`
|
||||
Directory *string `json:"directory" validate:"omitempty,min=1,max=255"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
Sort *int `json:"sort" validate:"omitempty,min=1"`
|
||||
FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
// ComponentsGetResponse defines the response structure for getting a list of components.
|
||||
type ComponentsGetResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []Rol_component `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
}
|
||||
|
||||
// ComponentCreateResponse defines the response structure for creating a component.
|
||||
type ComponentCreateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_component `json:"data"`
|
||||
}
|
||||
|
||||
// ComponentUpdateResponse defines the response structure for updating a component.
|
||||
type ComponentUpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_component `json:"data"`
|
||||
}
|
||||
|
||||
// ComponentDeleteResponse defines the response structure for deleting a component.
|
||||
type ComponentDeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Rol_componentFilter defines the structure for query parameters when filtering components.
|
||||
type Rol_componentFilter struct {
|
||||
PageID *int64 `json:"page_id,omitempty" form:"page_id"`
|
||||
Active *bool `json:"active,omitempty" form:"active"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Rol_pages represents the data structure for the role_access.rol_pages table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Rol_pages struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name string `json:"name," db:"name"`
|
||||
Icon sql.NullString `json:"icon,omitempty" db:"icon"`
|
||||
Url sql.NullString `json:"url,omitempty" db:"url"`
|
||||
Level int64 `json:"level" db:"level"`
|
||||
Sort int64 `json:"sort" db:"sort"`
|
||||
Parent models.NullableInt32 `json:"parent,omitempty" db:"parent"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
Components []Rol_component `json:"list_component,omitempty" db:"list_component"`
|
||||
Permissions []Rol_permission `json:"list_permission,omitempty" db:"permission"`
|
||||
}
|
||||
|
||||
type Rol_component struct {
|
||||
ID int64 `json:"id,omitempty" db:"id"`
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
Description sql.NullString `json:"description,omitempty" db:"description"`
|
||||
Directory string `json:"directory,omitempty" db:"directory"`
|
||||
Active bool `json:"active,omitempty" db:"active"`
|
||||
FkRolPagesID int64 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"`
|
||||
Sort int64 `json:"sort,omitempty" db:"sort"`
|
||||
}
|
||||
|
||||
type Rol_permission struct {
|
||||
ID int64 `json:"id,omitempty" db:"id"`
|
||||
Create sql.NullBool `json:"create,omitempty" db:"create"`
|
||||
Read sql.NullBool `json:"read,omitempty" db:"read"`
|
||||
Update sql.NullBool `json:"update,omitempty" db:"update"`
|
||||
Disable sql.NullBool `json:"disable,omitempty" db:"disable"` // Note: "disable" is a Go keyword, so "Disable" is used for the field name.
|
||||
Delete sql.NullBool `json:"delete,omitempty" db:"delete"`
|
||||
Active sql.NullBool `json:"active,omitempty" db:"active"`
|
||||
FkRolPagesID models.NullableInt32 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"`
|
||||
RoleKeycloak pq.StringArray `json:"role_keycloak,omitempty" db:"role_keycloak"` // Use NullString for optional text fields
|
||||
GroupKeycloak pq.StringArray `json:"group_keycloak,omitempty" db:"group_keycloak"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling for Rol_pages so NULL values don't appear in response
|
||||
func (r Rol_pages) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rol_pages
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
Parent *int `json:"parent,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Icon.Valid {
|
||||
aux.Icon = &r.Icon.String
|
||||
}
|
||||
if r.Url.Valid {
|
||||
aux.Url = &r.Url.String
|
||||
}
|
||||
if r.Parent.Valid {
|
||||
parent := int(r.Parent.Int32)
|
||||
aux.Parent = &parent
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (r Rol_component) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rol_component
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Description *string `json:"description,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Description.Valid {
|
||||
aux.Description = &r.Description.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (r Rol_permission) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rol_permission
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Create *bool `json:"create,omitempty"`
|
||||
Read *bool `json:"read,omitempty"`
|
||||
Update *bool `json:"update,omitempty"`
|
||||
Disable *bool `json:"disable,omitempty"`
|
||||
Delete *bool `json:"delete,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
FkRolPagesID *int `json:"fk_rol_pages_id,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Create.Valid {
|
||||
aux.Create = &r.Create.Bool
|
||||
}
|
||||
if r.Read.Valid {
|
||||
aux.Read = &r.Read.Bool
|
||||
}
|
||||
if r.Update.Valid {
|
||||
aux.Update = &r.Update.Bool
|
||||
}
|
||||
if r.Disable.Valid {
|
||||
aux.Disable = &r.Disable.Bool
|
||||
}
|
||||
if r.Delete.Valid {
|
||||
aux.Delete = &r.Delete.Bool
|
||||
}
|
||||
if r.Active.Valid {
|
||||
aux.Active = &r.Active.Bool
|
||||
}
|
||||
if r.FkRolPagesID.Valid {
|
||||
fkRolPagesID := int(r.FkRolPagesID.Int32)
|
||||
aux.FkRolPagesID = &fkRolPagesID
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper method to safely get Icon
|
||||
func (r *Rol_pages) GetIcon() string {
|
||||
if r.Icon.Valid {
|
||||
return r.Icon.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Helper method to safely get Url
|
||||
func (r *Rol_pages) GetUrl() string {
|
||||
if r.Url.Valid {
|
||||
return r.Url.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Helper method to safely get Parent
|
||||
func (r *Rol_pages) GetParent() int {
|
||||
if r.Parent.Valid {
|
||||
return int(r.Parent.Int32)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Helper method to safely get Description
|
||||
func (r *Rol_component) GetDescription() string {
|
||||
if r.Description.Valid {
|
||||
return r.Description.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Helper method to safely get Sort
|
||||
func (r *Rol_pages) GetSort() int64 {
|
||||
return r.Sort
|
||||
}
|
||||
|
||||
// Response struct for GET list
|
||||
type PagesGetResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []Rol_pages `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// Response struct for create
|
||||
type PagesCreateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_pages `json:"data"`
|
||||
}
|
||||
|
||||
// Request struct for create
|
||||
type PagesCreateRequest struct {
|
||||
//Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||
ID *int `json:"id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=20"`
|
||||
Icon *string `json:"icon" validate:"omitempty,min=1,max=20"`
|
||||
Url *string `json:"url" validate:"omitempty,min=1,max=100"`
|
||||
Level int `json:"level"`
|
||||
Sort int `json:"sort"`
|
||||
Parent *int `json:"parent" validate:"omitempty,min=1"`
|
||||
Active bool `json:"active"`
|
||||
Components []ComponentCreateRequest `json:"components,omitempty" validate:"omitempty,dive"`
|
||||
Permissions []PermissionCreateRequest `json:"permissions,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type ComponentCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=100"`
|
||||
Description *string `json:"description" validate:"omitempty,max=255"`
|
||||
Directory string `json:"directory" validate:"required,min=1,max=255"`
|
||||
Active bool `json:"active"`
|
||||
Sort int64 `json:"sort"`
|
||||
// FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type PermissionCreateRequest struct {
|
||||
Create *bool `json:"create" validate:"omitempty"`
|
||||
Read *bool `json:"read" validate:"omitempty"`
|
||||
Update *bool `json:"update" validate:"omitempty"`
|
||||
Disable *bool `json:"disable" validate:"omitempty"`
|
||||
Delete *bool `json:"delete" validate:"omitempty"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
// FkRolPagesID *int64 `json:"fk_rol_pages_id" validate:"omitempty,"`
|
||||
RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"`
|
||||
GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type ComponentUpdateRequest struct {
|
||||
ID *int `json:"id" validate:"required"`
|
||||
Name *string `json:"name" validate:"omitempty,min=1,max=100"`
|
||||
Description *string `json:"description" validate:"omitempty,max=255"`
|
||||
Directory *string `json:"directory" validate:"omitempty,min=1,max=255"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
Sort *int `json:"sort" validate:"omitempty,min=1"`
|
||||
FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type PermissionUpdateRequest struct {
|
||||
ID *int `json:"id" validate:"required"`
|
||||
Create *bool `json:"create" validate:"omitempty"`
|
||||
Read *bool `json:"read" validate:"omitempty"`
|
||||
Update *bool `json:"update" validate:"omitempty"`
|
||||
Disable *bool `json:"disable" validate:"omitempty"`
|
||||
Delete *bool `json:"delete" validate:"omitempty"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty"`
|
||||
RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"`
|
||||
GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// Response struct for update
|
||||
type PagesUpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_pages `json:"data"`
|
||||
}
|
||||
|
||||
// Update request
|
||||
type PagesUpdateRequest struct {
|
||||
ID *int `json:"-" validate:"required"`
|
||||
// Status string `json:"status" validate:"required,oneof=draft active inactive"`
|
||||
Name *string `json:"name" validate:"omitempty,min=1,max=20"`
|
||||
Icon *string `json:"icon" validate:"omitempty,min=1,max=20"`
|
||||
Url *string `json:"url" validate:"omitempty,min=1,max=100"`
|
||||
Level *int `json:"level" validate:"omitempty,min=1"`
|
||||
Sort *int `json:"sort" validate:"omitempty,min=1"`
|
||||
Parent *int `json:"parent" validate:"omitempty,min=1"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
Components []ComponentUpdateRequest `json:"components,omitempty" validate:"omitempty,dive"`
|
||||
Permissions []PermissionUpdateRequest `json:"permissions,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
// Response struct for delete
|
||||
type PagesDeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Filter struct for query parameters
|
||||
type Rol_pagesFilter struct {
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package patient
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// type Date time.Time
|
||||
|
||||
// func (d *Date) UnmarshalJSON(b []byte) error {
|
||||
// s := strings.Trim(string(b), "\"")
|
||||
// if s == "" || s == "null" {
|
||||
// *d = Date(time.Time{})
|
||||
// return nil
|
||||
// }
|
||||
// t, err := time.Parse("2006-01-02", s)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("invalid date format, expected YYYY-MM-DD: %w", err)
|
||||
// }
|
||||
// *d = Date(t)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // new: MarshalJSON so responses use "YYYY-MM-DD"
|
||||
// func (d Date) MarshalJSON() ([]byte, error) {
|
||||
// t := time.Time(d)
|
||||
// if t.IsZero() {
|
||||
// return []byte("null"), nil
|
||||
// }
|
||||
// return []byte("\"" + t.Format("2006-01-02") + "\""), nil
|
||||
// }
|
||||
|
||||
// // new: database/sql/driver.Valuer implementation so DB driver can encode Date
|
||||
// func (d Date) Value() (driver.Value, error) {
|
||||
// t := time.Time(d)
|
||||
// if t.IsZero() {
|
||||
// return nil, nil
|
||||
// }
|
||||
// // return time.Time so pq/pgx has an encode plan for date/timestamp types
|
||||
// return t, nil
|
||||
// }
|
||||
|
||||
// // new: sql.Scanner implementation so DB rows decode into Date
|
||||
// func (d *Date) Scan(src interface{}) error {
|
||||
// if src == nil {
|
||||
// *d = Date(time.Time{})
|
||||
// return nil
|
||||
// }
|
||||
// switch v := src.(type) {
|
||||
// case time.Time:
|
||||
// *d = Date(v)
|
||||
// return nil
|
||||
// case []byte:
|
||||
// s := string(v)
|
||||
// if t, err := time.Parse("2006-01-02", s); err == nil {
|
||||
// *d = Date(t)
|
||||
// return nil
|
||||
// }
|
||||
// if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
// *d = Date(t)
|
||||
// return nil
|
||||
// }
|
||||
// return fmt.Errorf("cannot parse date from []byte: %s", s)
|
||||
// case string:
|
||||
// s := v
|
||||
// if s == "" {
|
||||
// *d = Date(time.Time{})
|
||||
// return nil
|
||||
// }
|
||||
// if t, err := time.Parse("2006-01-02", s); err == nil {
|
||||
// *d = Date(t)
|
||||
// return nil
|
||||
// }
|
||||
// if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
// *d = Date(t)
|
||||
// return nil
|
||||
// }
|
||||
// return fmt.Errorf("cannot parse date from string: %s", s)
|
||||
// default:
|
||||
// return fmt.Errorf("unsupported scan type for Date: %T", src)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (d Date) ToTime() time.Time {
|
||||
// return time.Time(d)
|
||||
// }
|
||||
|
||||
// Patient represents the data structure for the patient table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Patient struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name sql.NullString `json:"name,omitempty" db:"name"`
|
||||
MedicalRecordNumber sql.NullString `json:"medical_record_number,omitempty" db:"medical_record_number"`
|
||||
PhoneNumber sql.NullString `json:"phone_number,omitempty" db:"phone_number"`
|
||||
Gender sql.NullString `json:"gender,omitempty" db:"gender"`
|
||||
BirthDate sql.NullTime `json:"birth_date,omitempty" db:"birth_date"`
|
||||
Address sql.NullString `json:"address,omitempty" db:"address"`
|
||||
Active sql.NullBool `json:"active,omitempty" db:"active"`
|
||||
FKSdProvinsiID models.NullableInt32 `json:"fk_sd_provinsi_id,omitempty" db:"fk_sd_provinsi_id"`
|
||||
FKSdKabupatenKotaID models.NullableInt32 `json:"fk_sd_kabupaten_kota_id,omitempty" db:"fk_sd_kabupaten_kota_id"`
|
||||
FKSdKecamatanID models.NullableInt32 `json:"fk_sd_kecamatan_id,omitempty" db:"fk_sd_kecamatan_id"`
|
||||
FKSdKelurahanID models.NullableInt32 `json:"fk_sd_kelurahan_id,omitempty" db:"fk_sd_kelurahan_id"`
|
||||
DsSdProvinsi sql.NullString `json:"ds_sd_provinsi,omitempty" db:"ds_sd_provinsi"`
|
||||
DsSdKabupatenKota sql.NullString `json:"ds_sd_kabupaten_kota,omitempty" db:"ds_sd_kabupaten_kota"`
|
||||
DsSdKecamatan sql.NullString `json:"ds_sd_kecamatan,omitempty" db:"ds_sd_kecamatan"`
|
||||
DsSdKelurahan sql.NullString `json:"ds_sd_kelurahan,omitempty" db:"ds_sd_kelurahan"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling untuk Patient agar NULL values tidak muncul di response
|
||||
func (r Patient) MarshalJSON() ([]byte, error) {
|
||||
type Alias Patient
|
||||
aux := &struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
MedicalRecordNumber *string `json:"medical_record_number,omitempty"`
|
||||
PhoneNumber *string `json:"phone_number,omitempty"`
|
||||
Gender *string `json:"gender,omitempty"`
|
||||
BirthDate *string `json:"birth_date,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id,omitempty"`
|
||||
FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id,omitempty"`
|
||||
FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id,omitempty"`
|
||||
FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id,omitempty"`
|
||||
DsSdProvinsi *string `json:"ds_sd_provinsi,omitempty"`
|
||||
DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota,omitempty"`
|
||||
DsSdKecamatan *string `json:"ds_sd_kecamatan,omitempty"`
|
||||
DsSdKelurahan *string `json:"ds_sd_kelurahan,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Name.Valid {
|
||||
aux.Name = &r.Name.String
|
||||
}
|
||||
if r.MedicalRecordNumber.Valid {
|
||||
aux.MedicalRecordNumber = &r.MedicalRecordNumber.String
|
||||
}
|
||||
if r.PhoneNumber.Valid {
|
||||
aux.PhoneNumber = &r.PhoneNumber.String
|
||||
}
|
||||
if r.Gender.Valid {
|
||||
aux.Gender = &r.Gender.String
|
||||
}
|
||||
if r.BirthDate.Valid {
|
||||
birthDateStr := r.BirthDate.Time.Format("2006-01-02")
|
||||
aux.BirthDate = &birthDateStr
|
||||
}
|
||||
if r.Address.Valid {
|
||||
aux.Address = &r.Address.String
|
||||
}
|
||||
if r.Active.Valid {
|
||||
aux.Active = &r.Active.Bool
|
||||
}
|
||||
if r.FKSdProvinsiID.Valid {
|
||||
fksp := int32(r.FKSdProvinsiID.Int32)
|
||||
aux.FKSdProvinsiID = &fksp
|
||||
}
|
||||
if r.FKSdKabupatenKotaID.Valid {
|
||||
fksk := int32(r.FKSdKabupatenKotaID.Int32)
|
||||
aux.FKSdKabupatenKotaID = &fksk
|
||||
}
|
||||
if r.FKSdKecamatanID.Valid {
|
||||
fksc := int32(r.FKSdKecamatanID.Int32)
|
||||
aux.FKSdKecamatanID = &fksc
|
||||
}
|
||||
if r.FKSdKelurahanID.Valid {
|
||||
fksl := int32(r.FKSdKelurahanID.Int32)
|
||||
aux.FKSdKelurahanID = &fksl
|
||||
}
|
||||
if r.DsSdProvinsi.Valid {
|
||||
aux.DsSdProvinsi = &r.DsSdProvinsi.String
|
||||
}
|
||||
if r.DsSdKabupatenKota.Valid {
|
||||
aux.DsSdKabupatenKota = &r.DsSdKabupatenKota.String
|
||||
}
|
||||
if r.DsSdKecamatan.Valid {
|
||||
aux.DsSdKecamatan = &r.DsSdKecamatan.String
|
||||
}
|
||||
if r.DsSdKelurahan.Valid {
|
||||
aux.DsSdKelurahan = &r.DsSdKelurahan.String
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper methods untuk mendapatkan nilai yang aman
|
||||
func (r *Patient) GetName() string {
|
||||
if r.Name.Valid {
|
||||
return r.Name.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response struct untuk GET by ID
|
||||
type PatientGetByIDResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Patient `json:"data"`
|
||||
}
|
||||
|
||||
// Enhanced GET response dengan pagination dan aggregation
|
||||
type PatientGetResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []Patient `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// Request struct untuk create
|
||||
type PatientCreateRequest struct {
|
||||
ID *int `json:"id"`
|
||||
Name *string `json:"name" validate:"min=1,max=100"`
|
||||
MedicalRecordNumber *string `json:"medical_record_number" validate:"min=1,max=20"`
|
||||
PhoneNumber *string `json:"phone_number" validate:"min=1,max=20"`
|
||||
Gender *string `json:"gender" validate:"max=1"`
|
||||
BirthDate *time.Time `json:"birth_date"`
|
||||
Address *string `json:"address" validate:"min=1,max=255"`
|
||||
Active *bool `json:"active"`
|
||||
FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id"`
|
||||
FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id"`
|
||||
FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id"`
|
||||
FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id"`
|
||||
DsSdProvinsi *string `json:"ds_sd_provinsi" validate:"min=1,max=255"`
|
||||
DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota" validate:"min=1,max=255"`
|
||||
DsSdKecamatan *string `json:"ds_sd_kecamatan" validate:"min=1,max=255"`
|
||||
DsSdKelurahan *string `json:"ds_sd_kelurahan" validate:"min=1,max=255"`
|
||||
}
|
||||
|
||||
type PatientPostRequest struct {
|
||||
ID int64 `json:"id" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// Response struct untuk create
|
||||
type PatientCreateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Patient `json:"data"`
|
||||
}
|
||||
|
||||
// Update request
|
||||
type PatientUpdateRequest struct {
|
||||
ID *int `json:"id" validate:"required,min=1"`
|
||||
Name *string `json:"name" validate:"min=1,max=100"`
|
||||
MedicalRecordNumber *string `json:"medical_record_number" validate:"min=1,max=20"`
|
||||
PhoneNumber *string `json:"phone_number" validate:"min=1,max=20"`
|
||||
Gender *string `json:"gender" validate:"max=1"`
|
||||
BirthDate *time.Time `json:"birth_date"`
|
||||
Address *string `json:"address" validate:"min=1,max=255"`
|
||||
Active *bool `json:"active"`
|
||||
FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id"`
|
||||
FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id"`
|
||||
FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id"`
|
||||
FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id"`
|
||||
DsSdProvinsi *string `json:"ds_sd_provinsi" validate:"max=255"`
|
||||
DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota" validate:"max=255"`
|
||||
DsSdKecamatan *string `json:"ds_sd_kecamatan" validate:"max=255"`
|
||||
DsSdKelurahan *string `json:"ds_sd_kelurahan" validate:"max=255"`
|
||||
}
|
||||
|
||||
// Response struct untuk update
|
||||
type PatientUpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Patient `json:"data"`
|
||||
}
|
||||
|
||||
// Response struct untuk delete
|
||||
type PatientDeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
MedicalRecordNumber string `json:"medical_record_number"`
|
||||
}
|
||||
|
||||
// Filter struct untuk query parameters
|
||||
type PatientFilter struct {
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Rol_permission represents the data structure for the role_access.rol_permission table
|
||||
// with proper null handling and optimized JSON marshaling.
|
||||
type Rol_permission struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Create sql.NullBool `json:"create,omitempty" db:"create"`
|
||||
Read sql.NullBool `json:"read,omitempty" db:"read"`
|
||||
Update sql.NullBool `json:"update,omitempty" db:"update"`
|
||||
Disable sql.NullBool `json:"disable,omitempty" db:"disable"` // "disable" is a Go keyword, so "Disable" is used for the field name.
|
||||
Delete sql.NullBool `json:"delete,omitempty" db:"delete"`
|
||||
Active sql.NullBool `json:"active,omitempty" db:"active"`
|
||||
FkRolPagesID models.NullableInt32 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"`
|
||||
RoleKeycloak pq.StringArray `json:"role_keycloak,omitempty" db:"role_keycloak"` // Use NullString for optional text fields
|
||||
GroupKeycloak pq.StringArray `json:"group_keycloak,omitempty" db:"group_keycloak"`
|
||||
}
|
||||
|
||||
// MarshalJSON for Rol_permission handles all sql.Null* fields
|
||||
// to ensure they appear as null, a Go value, or a string value in JSON.
|
||||
func (r Rol_permission) MarshalJSON() ([]byte, error) {
|
||||
type Alias Rol_permission
|
||||
aux := &struct {
|
||||
*Alias
|
||||
Create *bool `json:"create,omitempty"`
|
||||
Read *bool `json:"read,omitempty"`
|
||||
Update *bool `json:"update,omitempty"`
|
||||
Disable *bool `json:"disable,omitempty"`
|
||||
Delete *bool `json:"delete,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
FkRolPagesID *int `json:"fk_rol_pages_id,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Create.Valid {
|
||||
aux.Create = &r.Create.Bool
|
||||
}
|
||||
if r.Read.Valid {
|
||||
aux.Read = &r.Read.Bool
|
||||
}
|
||||
if r.Update.Valid {
|
||||
aux.Update = &r.Update.Bool
|
||||
}
|
||||
if r.Disable.Valid {
|
||||
aux.Disable = &r.Disable.Bool
|
||||
}
|
||||
if r.Delete.Valid {
|
||||
aux.Delete = &r.Delete.Bool
|
||||
}
|
||||
if r.Active.Valid {
|
||||
aux.Active = &r.Active.Bool
|
||||
}
|
||||
if r.FkRolPagesID.Valid {
|
||||
fkRolPagesID := int(r.FkRolPagesID.Int32)
|
||||
aux.FkRolPagesID = &fkRolPagesID
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REQUEST & RESPONSE STRUCTS FOR ROL_PERMISSION
|
||||
// =============================================================================
|
||||
|
||||
// PermissionCreateRequest defines the structure for creating a new permission.
|
||||
type PermissionCreateRequest struct {
|
||||
Create *bool `json:"create" validate:"omitempty"`
|
||||
Read *bool `json:"read" validate:"omitempty"`
|
||||
Update *bool `json:"update" validate:"omitempty"`
|
||||
Disable *bool `json:"disable" validate:"omitempty"`
|
||||
Delete *bool `json:"delete" validate:"omitempty"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
FkRolPagesID *int64 `json:"fk_rol_pages_id" validate:"omitempty,"`
|
||||
RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"`
|
||||
GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// PermissionUpdateRequest defines the structure for updating an existing permission.
|
||||
// ID is handled via URL parameter, so it's omitted from JSON with `json:"-"`.
|
||||
type PermissionUpdateRequest struct {
|
||||
ID *int `json:"-" validate:"required"` // ID is from URL param
|
||||
Create *bool `json:"create" validate:"omitempty"`
|
||||
Read *bool `json:"read" validate:"omitempty"`
|
||||
Update *bool `json:"update" validate:"omitempty"`
|
||||
Disable *bool `json:"disable" validate:"omitempty"`
|
||||
Delete *bool `json:"delete" validate:"omitempty"`
|
||||
Active *bool `json:"active" validate:"omitempty"`
|
||||
FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"omitempty"`
|
||||
RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"`
|
||||
GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// PermissionsGetResponse defines the response structure for getting a list of permissions.
|
||||
type PermissionsGetResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []Rol_permission `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
}
|
||||
|
||||
// PermissionCreateResponse defines the response structure for creating a permission.
|
||||
type PermissionCreateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_permission `json:"data"`
|
||||
}
|
||||
|
||||
// PermissionUpdateResponse defines the response structure for updating a permission.
|
||||
type PermissionUpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Rol_permission `json:"data"`
|
||||
}
|
||||
|
||||
// PermissionDeleteResponse defines the response structure for deleting a permission.
|
||||
type PermissionDeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Rol_permissionFilter defines the structure for query parameters when filtering permissions.
|
||||
type Rol_permissionFilter struct {
|
||||
PageID *int64 `json:"page_id,omitempty" form:"page_id"`
|
||||
Active *bool `json:"active,omitempty" form:"active"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
authHandlers "api-service/internal/handlers/auth"
|
||||
componentRolcomponentHandlers "api-service/internal/handlers/component"
|
||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||
|
||||
pagesRolpagesHandlers "api-service/internal/handlers/pages"
|
||||
pasienPasienHandlers "api-service/internal/handlers/pasien"
|
||||
patientMspatientHandlers "api-service/internal/handlers/patient"
|
||||
permissionRolpermissionHandlers "api-service/internal/handlers/permission"
|
||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||
"api-service/internal/middleware"
|
||||
services "api-service/internal/services/auth"
|
||||
@@ -137,6 +140,46 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
pasienPasienGroup.GET("/by-location", pasienPasienHandler.GetPasienByLocation)
|
||||
}
|
||||
|
||||
// Rol_pages endpoints
|
||||
pagesRolpagesHandler := pagesRolpagesHandlers.NewRol_pagesHandler()
|
||||
pagesRolpagesGroup := v1.Group("/pages")
|
||||
{
|
||||
pagesRolpagesGroup.GET("/", pagesRolpagesHandler.GetRol_pages)
|
||||
pagesRolpagesGroup.POST("/", pagesRolpagesHandler.CreateRol_pages)
|
||||
pagesRolpagesGroup.PUT("/:id", pagesRolpagesHandler.UpdateRol_pages)
|
||||
pagesRolpagesGroup.DELETE("/:id", pagesRolpagesHandler.DeleteRol_pages)
|
||||
}
|
||||
|
||||
// Rol_component endpoints
|
||||
componentRolcomponentHandler := componentRolcomponentHandlers.NewRol_componentHandler()
|
||||
componentRolcomponentGroup := v1.Group("/component")
|
||||
{
|
||||
componentRolcomponentGroup.GET("/", componentRolcomponentHandler.GetRol_components)
|
||||
componentRolcomponentGroup.POST("/", componentRolcomponentHandler.CreateRol_component)
|
||||
componentRolcomponentGroup.PUT("/:id", componentRolcomponentHandler.UpdateRol_component)
|
||||
componentRolcomponentGroup.DELETE("/:id", componentRolcomponentHandler.DeleteRol_component)
|
||||
}
|
||||
|
||||
// Rol_permission endpoints
|
||||
permissionRolpermissionHandler := permissionRolpermissionHandlers.NewRol_permissionHandler()
|
||||
permissionRolpermissionGroup := v1.Group("/permission")
|
||||
{
|
||||
permissionRolpermissionGroup.GET("/", permissionRolpermissionHandler.GetRol_permissions)
|
||||
permissionRolpermissionGroup.POST("/", permissionRolpermissionHandler.CreateRol_permission)
|
||||
permissionRolpermissionGroup.PUT("/:id", permissionRolpermissionHandler.UpdateRol_permission)
|
||||
permissionRolpermissionGroup.DELETE("/:id", permissionRolpermissionHandler.DeleteRol_permission)
|
||||
}
|
||||
|
||||
// Ms_patient endpoints
|
||||
patientMspatientHandler := patientMspatientHandlers.NewPatientHandler()
|
||||
patientMspatientGroup := v1.Group("/patient")
|
||||
{
|
||||
patientMspatientGroup.GET("/", patientMspatientHandler.GetMs_patient)
|
||||
patientMspatientGroup.POST("/", patientMspatientHandler.CreateMs_patient)
|
||||
patientMspatientGroup.PUT("/:medical_record_number", patientMspatientHandler.UpdateMs_patient)
|
||||
patientMspatientGroup.DELETE("/:medical_record_number", patientMspatientHandler.DeleteMs_patient)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROTECTED ROUTES (Authentication Required)
|
||||
// =============================================================================
|
||||
|
||||
@@ -61,6 +61,8 @@ const (
|
||||
OpArrayContains FilterOperator = "_array_contains"
|
||||
OpArrayNotContains FilterOperator = "_array_ncontains"
|
||||
OpArrayLength FilterOperator = "_array_length"
|
||||
OpArrayOverlap FilterOperator = "_array_overlap"
|
||||
OpArrayContainedBy FilterOperator = "_array_contained_by"
|
||||
)
|
||||
|
||||
// DynamicFilter represents a single filter condition
|
||||
@@ -627,6 +629,19 @@ func (qb *QueryBuilder) buildCTEClause(ctes []CTE) (string, []interface{}, error
|
||||
|
||||
// buildFromClause builds the FROM clause with optional alias
|
||||
func (qb *QueryBuilder) buildFromClause(table, alias string) string {
|
||||
// Check if the table name contains a dot (schema.table)
|
||||
if strings.Contains(table, ".") {
|
||||
parts := strings.Split(table, ".")
|
||||
if len(parts) == 2 {
|
||||
// Quote schema and table separately
|
||||
fromClause := fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1]))
|
||||
if alias != "" {
|
||||
fromClause += " " + qb.escapeIdentifier(alias)
|
||||
}
|
||||
return fromClause
|
||||
}
|
||||
}
|
||||
|
||||
fromClause := qb.escapeIdentifier(table)
|
||||
if alias != "" {
|
||||
fromClause += " " + qb.escapeIdentifier(alias)
|
||||
@@ -641,7 +656,18 @@ func (qb *QueryBuilder) buildSingleJoinClause(join Join) (string, string, string
|
||||
joinType = "INNER"
|
||||
}
|
||||
|
||||
table := qb.escapeIdentifier(join.Table)
|
||||
var table string
|
||||
if strings.Contains(join.Table, ".") {
|
||||
parts := strings.Split(join.Table, ".")
|
||||
if len(parts) == 2 {
|
||||
// Quote schema and table separately
|
||||
table = fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1]))
|
||||
} else {
|
||||
table = qb.escapeIdentifier(join.Table)
|
||||
}
|
||||
} else {
|
||||
table = qb.escapeIdentifier(join.Table)
|
||||
}
|
||||
if join.Alias != "" {
|
||||
table += " " + qb.escapeIdentifier(join.Alias)
|
||||
}
|
||||
@@ -803,7 +829,7 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in
|
||||
switch filter.Operator {
|
||||
case OpJsonContains, OpJsonNotContains, OpJsonExists, OpJsonNotExists, OpJsonEqual, OpJsonNotEqual:
|
||||
return qb.buildJsonFilterCondition(filter)
|
||||
case OpArrayContains, OpArrayNotContains, OpArrayLength:
|
||||
case OpArrayContains, OpArrayNotContains, OpArrayLength, OpArrayOverlap, OpArrayContainedBy:
|
||||
return qb.buildArrayFilterCondition(filter)
|
||||
}
|
||||
|
||||
@@ -1051,6 +1077,45 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string,
|
||||
default:
|
||||
return "", nil, fmt.Errorf("Array operations not supported for database type: %s", qb.dbType)
|
||||
}
|
||||
case OpArrayOverlap:
|
||||
// TAMBAHKAN INI
|
||||
switch qb.dbType {
|
||||
case DBTypePostgreSQL:
|
||||
expr = fmt.Sprintf("%s && ?", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeMySQL:
|
||||
// MySQL doesn't have native array overlap, use JSON_OVERLAPS if available
|
||||
expr = fmt.Sprintf("JSON_OVERLAPS(%s, ?)", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeSQLServer:
|
||||
// SQL Server workaround using EXISTS and OPENJSON
|
||||
expr = fmt.Sprintf("EXISTS (SELECT 1 FROM OPENJSON(%s) o1 CROSS JOIN OPENJSON(?) o2 WHERE o1.value = o2.value)", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeSQLite:
|
||||
// SQLite workaround using json_each
|
||||
expr = fmt.Sprintf("EXISTS (SELECT 1 FROM json_each(%s) j1 CROSS JOIN json_each(?) j2 WHERE j1.value = j2.value)", column)
|
||||
args = append(args, filter.Value)
|
||||
default:
|
||||
return "", nil, fmt.Errorf("Array overlap operations not supported for database type: %s", qb.dbType)
|
||||
}
|
||||
case OpArrayContainedBy:
|
||||
// TAMBAHKAN INI
|
||||
switch qb.dbType {
|
||||
case DBTypePostgreSQL:
|
||||
expr = fmt.Sprintf("%s <@ ?", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeMySQL:
|
||||
expr = fmt.Sprintf("JSON_CONTAINS(?, %s)", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeSQLServer:
|
||||
expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM OPENJSON(%s) WHERE value NOT IN (SELECT value FROM OPENJSON(?)))", column)
|
||||
args = append(args, filter.Value)
|
||||
case DBTypeSQLite:
|
||||
expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM json_each(%s) j1 WHERE j1.value NOT IN (SELECT j2.value FROM json_each(?) j2))", column)
|
||||
args = append(args, filter.Value)
|
||||
default:
|
||||
return "", nil, fmt.Errorf("Array contained_by operations not supported for database type: %s", qb.dbType)
|
||||
}
|
||||
case OpArrayNotContains:
|
||||
switch qb.dbType {
|
||||
case DBTypePostgreSQL:
|
||||
@@ -1114,11 +1179,54 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string,
|
||||
// =============================================================================
|
||||
|
||||
func (qb *QueryBuilder) ExecuteQuery(ctx context.Context, db *sqlx.DB, query DynamicQuery, dest interface{}) error {
|
||||
// sql, args, err := qb.BuildQuery(query)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// start := time.Now()
|
||||
// err = db.SelectContext(ctx, dest, sql, args...)
|
||||
// fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
|
||||
// return err
|
||||
sql, args, err := qb.BuildQuery(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
// Check if dest is a pointer to a slice of maps
|
||||
destValue := reflect.ValueOf(dest)
|
||||
if destValue.Kind() != reflect.Ptr || destValue.IsNil() {
|
||||
return fmt.Errorf("dest must be a non-nil pointer")
|
||||
}
|
||||
|
||||
destElem := destValue.Elem()
|
||||
if destElem.Kind() == reflect.Slice {
|
||||
sliceType := destElem.Type().Elem()
|
||||
if sliceType.Kind() == reflect.Map &&
|
||||
sliceType.Key().Kind() == reflect.String &&
|
||||
sliceType.Elem().Kind() == reflect.Interface {
|
||||
|
||||
// Handle slice of map[string]interface{}
|
||||
rows, err := db.QueryxContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
row := make(map[string]interface{})
|
||||
if err := rows.MapScan(row); err != nil {
|
||||
return err
|
||||
}
|
||||
destElem.Set(reflect.Append(destElem, reflect.ValueOf(row)))
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: use SelectContext
|
||||
err = db.SelectContext(ctx, dest, sql, args...)
|
||||
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
|
||||
return err
|
||||
@@ -2345,3 +2453,42 @@ func (mqb *MongoQueryBuilder) ExecuteDelete(ctx context.Context, collection *mon
|
||||
fmt.Printf("[DEBUG] MongoDB Delete executed in %v\n", time.Since(start))
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (qb *QueryBuilder) getOperatorSQL(op FilterOperator) (string, error) {
|
||||
switch op {
|
||||
case OpEqual:
|
||||
return "=", nil
|
||||
case OpNotEqual:
|
||||
return "!=", nil
|
||||
case OpLike:
|
||||
return "LIKE", nil
|
||||
case OpILike:
|
||||
return "ILIKE", nil
|
||||
case OpIn:
|
||||
return "IN", nil
|
||||
case OpNotIn:
|
||||
return "NOT IN", nil
|
||||
case OpGreaterThan:
|
||||
return ">", nil
|
||||
case OpGreaterThanEqual:
|
||||
return ">=", nil
|
||||
case OpLessThan:
|
||||
return "<", nil
|
||||
case OpLessThanEqual:
|
||||
return "<=", nil
|
||||
case OpNull:
|
||||
return "IS NULL", nil
|
||||
case OpNotNull:
|
||||
return "IS NOT NULL", nil
|
||||
case OpArrayContains:
|
||||
return "@>", nil
|
||||
case OpArrayNotContains:
|
||||
return "NOT @>", nil
|
||||
case OpArrayOverlap: // TAMBAHKAN INI
|
||||
return "&&", nil
|
||||
case OpArrayContainedBy: // BONUS
|
||||
return "<@", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operator: %s", op)
|
||||
}
|
||||
}
|
||||
|
||||
+140
-249
@@ -1,297 +1,213 @@
|
||||
global:
|
||||
module_name: "api-service"
|
||||
module_name: "service-antrean"
|
||||
output_dir: "internal/handlers"
|
||||
enable_swagger: true
|
||||
enable_logging: true
|
||||
database:
|
||||
default_connection: "postgres_satudata"
|
||||
default_connection: "db_antrean"
|
||||
timeout_seconds: 30
|
||||
|
||||
# services:
|
||||
# pasien:
|
||||
# name: "Manajemen Data Pasien"
|
||||
# category: "pasien"
|
||||
# package: "pasien"
|
||||
# description: "API untuk mengelola data pasien dengan informasi lokasi lengkap"
|
||||
# base_url: ""
|
||||
# timeout: 30
|
||||
# retry_count: 3
|
||||
# table_name: "m_pasien"
|
||||
|
||||
services:
|
||||
pasien:
|
||||
name: "Manajemen Data Pasien"
|
||||
category: "pasien"
|
||||
package: "pasien"
|
||||
description: "API untuk mengelola data pasien dengan informasi lokasi lengkap"
|
||||
rol_pages:
|
||||
name: "Manajemen Pages Role"
|
||||
category: "pages"
|
||||
package: "pages"
|
||||
description: "Pages service for role management"
|
||||
base_url: ""
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
table_name: "m_pasien"
|
||||
table_name: "role_access.rol_pages"
|
||||
|
||||
# Define all columns once for reuse
|
||||
schema:
|
||||
columns:
|
||||
- name: "id"
|
||||
type: "serial4"
|
||||
type: "int4"
|
||||
nullable: false
|
||||
go_type: "int32"
|
||||
go_type: "int"
|
||||
primary_key: true
|
||||
unique: true
|
||||
description: "Primary key for schedule"
|
||||
- name: "nomr"
|
||||
description: "Primary key for pages"
|
||||
- name: "name"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
nullable: false
|
||||
go_type: "string"
|
||||
validation: "required,min=1,max=20"
|
||||
searchable: true
|
||||
unique: true
|
||||
description: "Nomor Rekam Medis"
|
||||
- name: "status"
|
||||
description: "Name of the page"
|
||||
- name: "icon"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Status pasien (A = Aktif, I = Inaktif)"
|
||||
- name: "title"
|
||||
type: "varchar"
|
||||
validation: "omitempty,min=1,max=20"
|
||||
description: "Icon for the page"
|
||||
- name: "url"
|
||||
type: "text"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Gelar pasien (Tn, Ny, Sdr, dll)"
|
||||
- name: "nama"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
validation: "required,min=1,max=100"
|
||||
validation: "omitempty,min=1,max=100"
|
||||
searchable: true
|
||||
description: "Nama lengkap pasien"
|
||||
- name: "tempat"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Tempat lahir pasien"
|
||||
- name: "tgllahir"
|
||||
type: "date"
|
||||
nullable: true
|
||||
go_type: "time.Time"
|
||||
description: "Tanggal lahir pasien"
|
||||
- name: "jeniskelamin"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
validation: "oneof=L P"
|
||||
description: "Jenis kelamin (L/P)"
|
||||
- name: "alamat"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Alamat lengkap pasien"
|
||||
- name: "kelurahan"
|
||||
type: "int8"
|
||||
nullable: true
|
||||
go_type: "int64"
|
||||
description: "ID Kelurahan"
|
||||
- name: "kdkecamatan"
|
||||
type: "int4"
|
||||
nullable: true
|
||||
go_type: "int32"
|
||||
description: "ID Kecamatan"
|
||||
- name: "kota"
|
||||
type: "int4"
|
||||
nullable: true
|
||||
go_type: "int32"
|
||||
description: "ID Kota"
|
||||
- name: "kdprovinsi"
|
||||
type: "int4"
|
||||
nullable: true
|
||||
go_type: "int32"
|
||||
description: "ID Provinsi"
|
||||
- name: "agama"
|
||||
type: "int4"
|
||||
nullable: true
|
||||
go_type: "int32"
|
||||
description: "ID Agama"
|
||||
- name: "no_kartu"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "URL of the page"
|
||||
- name: "level"
|
||||
type: "int2"
|
||||
nullable: false
|
||||
go_type: "int"
|
||||
searchable: true
|
||||
unique: true
|
||||
description: "Nomor kartu identitas"
|
||||
- name: "noktp_baru"
|
||||
type: "varchar"
|
||||
description: "Level of the page in hierarchy"
|
||||
- name: "sort"
|
||||
type: "int2"
|
||||
nullable: false
|
||||
go_type: "int"
|
||||
description: "Sort order of the page"
|
||||
- name: "parent"
|
||||
type: "int4"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Nomor KTP baru"
|
||||
- name: "created_at"
|
||||
type: "timestamp"
|
||||
nullable: true
|
||||
go_type: "time.Time"
|
||||
system_field: true
|
||||
description: "Tanggal pembuatan record"
|
||||
- name: "updated_at"
|
||||
type: "timestamp"
|
||||
nullable: true
|
||||
go_type: "time.Time"
|
||||
system_field: true
|
||||
description: "Tanggal update record"
|
||||
go_type: "int"
|
||||
validation: "omitempty,min=1"
|
||||
description: "Parent page ID"
|
||||
- name: "active"
|
||||
type: "bool"
|
||||
nullable: false
|
||||
go_type: "bool"
|
||||
description: "Active status of the page"
|
||||
|
||||
# Define relationships with other tables
|
||||
relationships:
|
||||
- name: "provinsi"
|
||||
table: "m_provinsi"
|
||||
foreign_key: "kdprovinsi"
|
||||
local_key: "idprovinsi"
|
||||
- name: "component"
|
||||
table: "role_access.rol_component"
|
||||
foreign_key: "fk_rol_pages_id"
|
||||
local_key: "id"
|
||||
columns:
|
||||
- name: "idprovinsi"
|
||||
- name: "id"
|
||||
type: "int4"
|
||||
nullable: false
|
||||
go_type: "int32"
|
||||
go_type: "int"
|
||||
primary_key: true
|
||||
- name: "namaprovinsi"
|
||||
unique: true
|
||||
description: "Primary key for component"
|
||||
- name: "name"
|
||||
type: "varchar"
|
||||
nullable: false
|
||||
go_type: "string"
|
||||
validation: "required,min=1,max=100"
|
||||
searchable: true
|
||||
description: "Name of the component"
|
||||
- name: "description"
|
||||
type: "text"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Nama provinsi"
|
||||
- name: "kota"
|
||||
table: "m_kota"
|
||||
foreign_key: "kota"
|
||||
local_key: "idkota"
|
||||
columns:
|
||||
- name: "idkota"
|
||||
validation: "omitempty,min=1,max=100"
|
||||
description: "Description of the component"
|
||||
- name: "directory"
|
||||
type: "text"
|
||||
nullable: false
|
||||
go_type: "string"
|
||||
validation: "required,min=1,max=100"
|
||||
searchable: true
|
||||
description: "Directory path of the component"
|
||||
- name: "active"
|
||||
type: "bool"
|
||||
nullable: false
|
||||
go_type: "bool"
|
||||
description: "Active status of the component"
|
||||
- name: "fk_rol_pages_id"
|
||||
type: "int4"
|
||||
nullable: false
|
||||
go_type: "int32"
|
||||
primary_key: true
|
||||
- name: "namakota"
|
||||
type: "varchar"
|
||||
go_type: "int"
|
||||
searchable: true
|
||||
description: "Foreign key to rol_pages"
|
||||
- name: "sort"
|
||||
type: "int2"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Nama kota"
|
||||
- name: "kecamatan"
|
||||
table: "m_kecamatan"
|
||||
foreign_key: "kdkecamatan"
|
||||
local_key: "idkecamatan"
|
||||
columns:
|
||||
- name: "idkecamatan"
|
||||
type: "int8"
|
||||
nullable: false
|
||||
go_type: "int64"
|
||||
primary_key: true
|
||||
- name: "namakecamatan"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Nama kecamatan"
|
||||
- name: "kelurahan"
|
||||
table: "m_kelurahan"
|
||||
foreign_key: "kelurahan"
|
||||
local_key: "idkelurahan"
|
||||
columns:
|
||||
- name: "idkelurahan"
|
||||
type: "int8"
|
||||
nullable: false
|
||||
go_type: "int64"
|
||||
primary_key: true
|
||||
- name: "namakelurahan"
|
||||
type: "varchar"
|
||||
nullable: true
|
||||
go_type: "string"
|
||||
description: "Nama kelurahan"
|
||||
go_type: "int"
|
||||
validation: "omitempty,min=1"
|
||||
description: "Sort order of the component"
|
||||
|
||||
# Define reusable field groups
|
||||
field_groups:
|
||||
base_fields: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin"]
|
||||
location_fields: ["alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi"]
|
||||
identity_fields: ["agama", "no_kartu", "noktp_baru"]
|
||||
all_fields: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi", "agama", "no_kartu", "noktp_baru"]
|
||||
with_location_names: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "namakelurahan", "kdkecamatan", "namakecamatan", "kota", "namakota", "kdprovinsi", "namaprovinsi", "agama", "no_kartu", "noktp_baru"]
|
||||
base_fields: ["id", "name", "icon", "url", "level", "sort", "parent", "active"]
|
||||
# location_fields: ["alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi"]
|
||||
# identity_fields: ["agama", "no_kartu", "noktp_baru"]
|
||||
all_fields: ["id", "name", "icon", "url", "level", "sort", "parent", "active"]
|
||||
# with_location_names: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "namakelurahan", "kdkecamatan", "namakecamatan", "kota", "namakota", "kdprovinsi", "namaprovinsi", "agama", "no_kartu", "noktp_baru"]
|
||||
|
||||
# Define endpoints with reusable configurations
|
||||
endpoints:
|
||||
list:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
handler_folder: "pages"
|
||||
handler_file: "pages.go"
|
||||
methods: ["GET"]
|
||||
path: "/"
|
||||
description: "Get list of pasien with pagination and filters"
|
||||
summary: "Get Pasien List"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
description: "Get list of pages with pagination and filters"
|
||||
summary: "Get Pages List"
|
||||
tags: ["Pages"]
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
has_pagination: true
|
||||
has_filter: true
|
||||
has_search: true
|
||||
has_stats: true
|
||||
fields: "with_location_names"
|
||||
response_model: "PasienGetResponse"
|
||||
|
||||
get_by_id:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
methods: ["GET"]
|
||||
path: "/:id"
|
||||
description: "Get pasien by ID"
|
||||
summary: "Get Pasien by ID"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
fields: "with_location_names"
|
||||
response_model: "PasienGetByIDResponse"
|
||||
|
||||
get_by_nomr:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
methods: ["GET"]
|
||||
path: "/nomr/:nomr"
|
||||
description: "Get pasien by Nomr"
|
||||
summary: "Get Pasien by Nomr"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
fields: "with_location_names"
|
||||
response_model: "PasienGetByNomrResponse"
|
||||
fields: "base_fields"
|
||||
response_model: "PagesGetResponse"
|
||||
|
||||
create:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
handler_folder: "pages"
|
||||
handler_file: "pages.go"
|
||||
methods: ["POST"]
|
||||
path: "/"
|
||||
description: "Create a new pasien"
|
||||
summary: "Create Pasien"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
fields: "all_fields"
|
||||
request_model: "PasienCreateRequest"
|
||||
response_model: "PasienCreateResponse"
|
||||
description: "Create a new page"
|
||||
summary: "Create Page"
|
||||
tags: ["Pages"]
|
||||
fields: "base_fields"
|
||||
request_model: "PagesCreateRequest"
|
||||
response_model: "PagesCreateResponse"
|
||||
|
||||
update:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
handler_folder: "pages"
|
||||
handler_file: "pages.go"
|
||||
methods: ["PUT"]
|
||||
path: "/:nomr"
|
||||
description: "Update an existing pasien"
|
||||
summary: "Update Pasien"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
fields: "all_fields"
|
||||
request_model: "PasienUpdateRequest"
|
||||
response_model: "PasienUpdateResponse"
|
||||
path: "/:id"
|
||||
description: "Update an existing page"
|
||||
summary: "Update Page"
|
||||
tags: ["Pages"]
|
||||
fields: "base_fields"
|
||||
request_model: "PagesUpdateRequest"
|
||||
response_model: "PagesUpdateResponse"
|
||||
|
||||
delete:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
handler_folder: "pages"
|
||||
handler_file: "pages.go"
|
||||
methods: ["DELETE"]
|
||||
path: "/:nomr"
|
||||
description: "Delete a pasien"
|
||||
summary: "Delete Pasien"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
path: "/:id"
|
||||
description: "Delete a page"
|
||||
summary: "Delete Page"
|
||||
tags: ["Pages"]
|
||||
soft_delete: true
|
||||
response_model: "PasienDeleteResponse"
|
||||
response_model: "PagesDeleteResponse"
|
||||
|
||||
dynamic:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
methods: ["GET"]
|
||||
path: "/dynamic"
|
||||
description: "Get pasien with dynamic filtering"
|
||||
summary: "Get Pasien Dynamic"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
has_dynamic: true
|
||||
fields: "with_location_names"
|
||||
response_model: "PasienGetResponse"
|
||||
# dynamic:
|
||||
# handler_folder: "pages"
|
||||
# handler_file: "pages.go"
|
||||
# methods: ["GET"]
|
||||
# path: "/pages/dynamic"
|
||||
# description: "Get pages with dynamic filtering"
|
||||
# summary: "Get Pages Dynamic"
|
||||
# tags: ["Pages"]
|
||||
# require_auth: true
|
||||
# has_dynamic: true
|
||||
# fields: "base_fields"
|
||||
# response_model: "PagesGetResponse"
|
||||
|
||||
# search:
|
||||
# handler_folder: "pasien"
|
||||
@@ -317,31 +233,6 @@ services:
|
||||
# require_auth: true
|
||||
# has_stats: true
|
||||
# response_model: "AggregateData"
|
||||
|
||||
by_location:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
methods: ["GET"]
|
||||
path: "/by-location"
|
||||
description: "Get pasien by location (provinsi, kota, kecamatan, kelurahan)"
|
||||
summary: "Get Pasien by Location"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
has_filter: true
|
||||
fields: "with_location_names"
|
||||
response_model: "PasienGetResponse"
|
||||
|
||||
by_age:
|
||||
handler_folder: "pasien"
|
||||
handler_file: "pasien.go"
|
||||
methods: ["GET"]
|
||||
path: "/by-age"
|
||||
description: "Get pasien statistics by age group"
|
||||
summary: "Get Pasien by Age Group"
|
||||
tags: ["Pasien"]
|
||||
require_auth: true
|
||||
has_stats: true
|
||||
response_model: "PasienAgeStatsResponse"
|
||||
|
||||
# schedule:
|
||||
# name: "Jadwal Dokter"
|
||||
|
||||
Reference in New Issue
Block a user