Intial commit

This commit is contained in:
2026-01-05 11:56:19 +07:00
parent db5a19e9cc
commit 2878d784a0
16 changed files with 11883 additions and 260 deletions
+1 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+2574 -1
View File
File diff suppressed because it is too large Load Diff
+1728 -1
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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,
}
}
+107
View File
@@ -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"`
}
+274
View File
@@ -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"`
}
+280
View File
@@ -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"`
}
+44 -1
View File
@@ -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)
// =============================================================================
+149 -2
View File
@@ -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
View File
@@ -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"