mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-12 22:14:26 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e220ab3d34 | ||
|
|
6a0297713a | ||
|
|
6ea200bb2b | ||
|
|
987244019c | ||
|
|
62a8e56f1b | ||
|
|
d8df1bdac2 |
@@ -3,6 +3,8 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/config"
|
||||
)
|
||||
|
||||
// CORSConfig holds CORS configuration
|
||||
@@ -15,8 +17,27 @@ type CORSConfig struct {
|
||||
|
||||
// DefaultCORSConfig returns a default CORS configuration suitable for HeadSpec
|
||||
func DefaultCORSConfig() CORSConfig {
|
||||
configManager := config.GetConfigManager()
|
||||
cfg, _ := configManager.GetConfig()
|
||||
hosts := make([]string, 0)
|
||||
// hosts = append(hosts, "*")
|
||||
|
||||
_, _, ipsList := config.GetIPs()
|
||||
|
||||
for i := range cfg.Servers.Instances {
|
||||
server := cfg.Servers.Instances[i]
|
||||
hosts = append(hosts, fmt.Sprintf("http://%s:%d", server.Host, server.Port))
|
||||
hosts = append(hosts, fmt.Sprintf("https://%s:%d", server.Host, server.Port))
|
||||
hosts = append(hosts, fmt.Sprintf("http://%s:%d", "localhost", server.Port))
|
||||
hosts = append(hosts, server.ExternalURLs...)
|
||||
for _, ip := range ipsList {
|
||||
hosts = append(hosts, fmt.Sprintf("http://%s:%d", ip.String(), server.Port))
|
||||
hosts = append(hosts, fmt.Sprintf("https://%s:%d", ip.String(), server.Port))
|
||||
}
|
||||
}
|
||||
|
||||
return CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedOrigins: hosts,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: GetHeadSpecHeaders(),
|
||||
MaxAge: 86400, // 24 hours
|
||||
|
||||
@@ -73,6 +73,9 @@ type ServerInstanceConfig struct {
|
||||
|
||||
// Tags for organization and filtering
|
||||
Tags map[string]string `mapstructure:"tags"`
|
||||
|
||||
// ExternalURLs are additional URLs that this server instance is accessible from (for CORS) for proxy setups
|
||||
ExternalURLs []string `mapstructure:"external_urls"`
|
||||
}
|
||||
|
||||
// TracingConfig holds OpenTelemetry tracing configuration
|
||||
|
||||
@@ -12,6 +12,16 @@ type Manager struct {
|
||||
v *viper.Viper
|
||||
}
|
||||
|
||||
var configInstance *Manager
|
||||
|
||||
// GetConfigManager returns a singleton configuration manager instance
|
||||
func GetConfigManager() *Manager {
|
||||
if configInstance == nil {
|
||||
configInstance = NewManager()
|
||||
}
|
||||
return configInstance
|
||||
}
|
||||
|
||||
// NewManager creates a new configuration manager with defaults
|
||||
func NewManager() *Manager {
|
||||
v := viper.New()
|
||||
@@ -32,7 +42,8 @@ func NewManager() *Manager {
|
||||
// Set default values
|
||||
setDefaults(v)
|
||||
|
||||
return &Manager{v: v}
|
||||
configInstance = &Manager{v: v}
|
||||
return configInstance
|
||||
}
|
||||
|
||||
// NewManagerWithOptions creates a new configuration manager with custom options
|
||||
|
||||
@@ -2,6 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ApplyGlobalDefaults applies global server defaults to this instance
|
||||
@@ -105,3 +108,42 @@ func (sc *ServersConfig) GetDefault() (*ServerInstanceConfig, error) {
|
||||
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
// GetIPs - GetIP for pc
|
||||
func GetIPs() (hostname string, ipList string, ipNetList []net.IP) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("Recovered in GetIPs", err)
|
||||
}
|
||||
}()
|
||||
hostname, _ = os.Hostname()
|
||||
ipaddrlist := make([]net.IP, 0)
|
||||
iplist := ""
|
||||
addrs, err := net.LookupIP(hostname)
|
||||
if err != nil {
|
||||
return hostname, iplist, ipaddrlist
|
||||
}
|
||||
|
||||
for _, a := range addrs {
|
||||
// cfg.LogInfo("\nFound IP Host Address: %s", a)
|
||||
if strings.Contains(a.String(), "127.0.0.1") {
|
||||
continue
|
||||
}
|
||||
iplist = fmt.Sprintf("%s,%s", iplist, a)
|
||||
ipaddrlist = append(ipaddrlist, a)
|
||||
}
|
||||
if iplist == "" {
|
||||
iff, _ := net.InterfaceAddrs()
|
||||
for _, a := range iff {
|
||||
// cfg.LogInfo("\nFound IP Address: %s", a)
|
||||
if strings.Contains(a.String(), "127.0.0.1") {
|
||||
continue
|
||||
}
|
||||
iplist = fmt.Sprintf("%s,%s", iplist, a)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
iplist = strings.TrimLeft(iplist, ",")
|
||||
return hostname, iplist, ipaddrlist
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||
@@ -1099,9 +1101,25 @@ func normalizePostgresValue(value interface{}) interface{} {
|
||||
case map[string]interface{}:
|
||||
// Recursively normalize nested maps
|
||||
return normalizePostgresTypes(v)
|
||||
|
||||
case string:
|
||||
var jsonObj interface{}
|
||||
if err := json.Unmarshal([]byte(v), &jsonObj); err == nil {
|
||||
// It's valid JSON, return as json.RawMessage so it's not double-encoded
|
||||
return json.RawMessage(v)
|
||||
}
|
||||
return v
|
||||
case uuid.UUID:
|
||||
return v.String()
|
||||
case time.Time:
|
||||
return v.Format(time.RFC3339)
|
||||
case bool, int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64:
|
||||
return v
|
||||
default:
|
||||
// For other types (int, float, string, bool, etc.), return as-is
|
||||
// For other types (int, float, bool, etc.), return as-is
|
||||
// Check stringers
|
||||
if str, ok := v.(fmt.Stringer); ok {
|
||||
return str.String()
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,20 @@ func ExtractTableNameOnly(fullName string) string {
|
||||
|
||||
return fullName[startIndex:]
|
||||
}
|
||||
|
||||
// GetPointerElement returns the element type if the provided reflect.Type is a pointer.
|
||||
// If the type is a slice of pointers, it returns the element type of the pointer within the slice.
|
||||
// If neither condition is met, it returns the original type.
|
||||
func GetPointerElement(v reflect.Type) reflect.Type {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
return v.Elem()
|
||||
}
|
||||
if v.Kind() == reflect.Slice && v.Elem().Kind() == reflect.Ptr {
|
||||
subElem := v.Elem()
|
||||
if subElem.Elem().Kind() == reflect.Ptr {
|
||||
return subElem.Elem().Elem()
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -584,11 +584,23 @@ func ExtractSourceColumn(colName string) string {
|
||||
}
|
||||
|
||||
// ToSnakeCase converts a string from CamelCase to snake_case
|
||||
// Handles consecutive uppercase letters (acronyms) correctly:
|
||||
// "HTTPServer" -> "http_server", "UserID" -> "user_id", "MyHTTPServer" -> "my_http_server"
|
||||
func ToSnakeCase(s string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range s {
|
||||
runes := []rune(s)
|
||||
|
||||
for i, r := range runes {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
result.WriteRune('_')
|
||||
// Add underscore if:
|
||||
// 1. Previous character is lowercase, OR
|
||||
// 2. Next character is lowercase (transition from acronym to word)
|
||||
prevIsLower := runes[i-1] >= 'a' && runes[i-1] <= 'z'
|
||||
nextIsLower := i+1 < len(runes) && runes[i+1] >= 'a' && runes[i+1] <= 'z'
|
||||
|
||||
if prevIsLower || nextIsLower {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
@@ -961,7 +973,7 @@ func MapToStruct(dataMap map[string]interface{}, target interface{}) error {
|
||||
// 4. Field name variations
|
||||
columnNames = append(columnNames, field.Name)
|
||||
columnNames = append(columnNames, strings.ToLower(field.Name))
|
||||
columnNames = append(columnNames, ToSnakeCase(field.Name))
|
||||
// columnNames = append(columnNames, ToSnakeCase(field.Name))
|
||||
|
||||
// Map all column name variations to this field index
|
||||
for _, colName := range columnNames {
|
||||
@@ -1067,7 +1079,7 @@ func setFieldValue(field reflect.Value, value interface{}) error {
|
||||
case string:
|
||||
field.SetBytes([]byte(v))
|
||||
return nil
|
||||
case map[string]interface{}, []interface{}:
|
||||
case map[string]interface{}, []interface{}, []*any, map[string]*any:
|
||||
// Marshal complex types to JSON for SqlJSONB fields
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
@@ -1077,6 +1089,11 @@ func setFieldValue(field reflect.Value, value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Handle slice-to-slice conversions (e.g., []interface{} to []*SomeModel)
|
||||
if valueReflect.Kind() == reflect.Slice {
|
||||
return convertSlice(field, valueReflect)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle struct types (like SqlTimeStamp, SqlDate, SqlTime which wrap SqlNull[time.Time])
|
||||
@@ -1156,6 +1173,92 @@ func setFieldValue(field reflect.Value, value interface{}) error {
|
||||
return fmt.Errorf("cannot convert %v to %v", valueReflect.Type(), field.Type())
|
||||
}
|
||||
|
||||
// convertSlice converts a source slice to a target slice type, handling element-wise conversions
|
||||
// Supports converting []interface{} to slices of structs or pointers to structs
|
||||
func convertSlice(targetSlice reflect.Value, sourceSlice reflect.Value) error {
|
||||
if sourceSlice.Kind() != reflect.Slice || targetSlice.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("both source and target must be slices")
|
||||
}
|
||||
|
||||
// Get the element type of the target slice
|
||||
targetElemType := targetSlice.Type().Elem()
|
||||
sourceLen := sourceSlice.Len()
|
||||
|
||||
// Create a new slice with the same length as the source
|
||||
newSlice := reflect.MakeSlice(targetSlice.Type(), sourceLen, sourceLen)
|
||||
|
||||
// Convert each element
|
||||
for i := 0; i < sourceLen; i++ {
|
||||
sourceElem := sourceSlice.Index(i)
|
||||
targetElem := newSlice.Index(i)
|
||||
|
||||
// Get the actual value from the source element
|
||||
var sourceValue interface{}
|
||||
if sourceElem.CanInterface() {
|
||||
sourceValue = sourceElem.Interface()
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle nil elements
|
||||
if sourceValue == nil {
|
||||
// For pointer types, nil is valid
|
||||
if targetElemType.Kind() == reflect.Ptr {
|
||||
targetElem.Set(reflect.Zero(targetElemType))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If target element type is a pointer to struct, we need to create new instances
|
||||
if targetElemType.Kind() == reflect.Ptr {
|
||||
// Create a new instance of the pointed-to type
|
||||
newElemPtr := reflect.New(targetElemType.Elem())
|
||||
|
||||
// Convert the source value to the struct
|
||||
switch sv := sourceValue.(type) {
|
||||
case map[string]interface{}:
|
||||
// Source is a map, use MapToStruct to populate the new instance
|
||||
if err := MapToStruct(sv, newElemPtr.Interface()); err != nil {
|
||||
return fmt.Errorf("failed to convert element %d: %w", i, err)
|
||||
}
|
||||
default:
|
||||
// Try direct conversion or setFieldValue
|
||||
if err := setFieldValue(newElemPtr.Elem(), sourceValue); err != nil {
|
||||
return fmt.Errorf("failed to convert element %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
targetElem.Set(newElemPtr)
|
||||
} else if targetElemType.Kind() == reflect.Struct {
|
||||
// Target element is a struct (not a pointer)
|
||||
switch sv := sourceValue.(type) {
|
||||
case map[string]interface{}:
|
||||
// Use MapToStruct to populate the element
|
||||
elemPtr := targetElem.Addr()
|
||||
if elemPtr.CanInterface() {
|
||||
if err := MapToStruct(sv, elemPtr.Interface()); err != nil {
|
||||
return fmt.Errorf("failed to convert element %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Try direct conversion
|
||||
if err := setFieldValue(targetElem, sourceValue); err != nil {
|
||||
return fmt.Errorf("failed to convert element %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other types, use setFieldValue
|
||||
if err := setFieldValue(targetElem, sourceValue); err != nil {
|
||||
return fmt.Errorf("failed to convert element %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the converted slice to the target field
|
||||
targetSlice.Set(newSlice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToInt64 attempts to convert various types to int64
|
||||
func convertToInt64(value interface{}) (int64, bool) {
|
||||
switch v := value.(type) {
|
||||
|
||||
@@ -702,7 +702,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
pkName := reflection.GetPrimaryKeyName(model)
|
||||
|
||||
// First, read the existing record from the database
|
||||
existingRecord := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||
selectQuery := h.db.NewSelect().Model(existingRecord)
|
||||
|
||||
// Apply conditions to select
|
||||
@@ -850,7 +850,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
for _, item := range updates {
|
||||
if itemID, ok := item["id"]; ok {
|
||||
// First, read the existing record
|
||||
existingRecord := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -958,7 +958,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if itemID, ok := itemMap["id"]; ok {
|
||||
// First, read the existing record
|
||||
existingRecord := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
||||
@@ -1240,7 +1240,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
||||
txNestedProcessor := common.NewNestedCUDProcessor(tx, h.registry, h)
|
||||
|
||||
// First, read the existing record from the database
|
||||
existingRecord := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -1294,9 +1294,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
||||
// Populate model instance from dataMap to preserve custom types (like SqlJSONB)
|
||||
// Get the type of the model, handling both pointer and non-pointer types
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
modelType = reflection.GetPointerElement(modelType)
|
||||
modelInstance := reflect.New(modelType).Interface()
|
||||
if err := reflection.MapToStruct(dataMap, modelInstance); err != nil {
|
||||
return fmt.Errorf("failed to populate model from data: %w", err)
|
||||
@@ -1600,9 +1598,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
||||
|
||||
// First, fetch the record that will be deleted
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
modelType = reflection.GetPointerElement(modelType)
|
||||
recordToDelete := reflect.New(modelType).Interface()
|
||||
|
||||
selectQuery := h.db.NewSelect().Model(recordToDelete).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||
|
||||
Reference in New Issue
Block a user