Compare commits

...

7 Commits

Author SHA1 Message Date
Hein
a980201d21 feat(spectypes): enhance SqlNull to support float and int types
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -27m18s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -26m43s
Build , Vet Test, and Lint / Lint Code (push) Successful in -26m48s
Build , Vet Test, and Lint / Build (push) Successful in -27m4s
Tests / Unit Tests (push) Successful in -27m26s
Tests / Integration Tests (push) Failing after -27m41s
* Add handling for float32 and float64 in Scan method.
* Implement parsing for integer types in Scan and FromString methods.
* Improve flexibility of SqlNull for various numeric inputs.
2026-01-13 15:09:56 +02:00
Hein
276854768e feat(dbmanager): add support for existing SQL connections
* Introduced NewConnectionFromDB function to create connections from existing *sql.DB instances.
* Added ExistingDBProvider to wrap existing database connections for dbmanager features.
* Implemented tests for NewConnectionFromDB and ExistingDBProvider functionalities.
2026-01-13 12:50:12 +02:00
Hein
cf6a81e805 feat(reflection): add tests for standard SQL null types
* Implement tests for mapping standard library sql.Null* types to struct.
* Verify handling of valid and nil values for sql.NullInt64, sql.NullString, sql.NullFloat64, sql.NullBool, and sql.NullTime.
* Ensure correct error handling and type conversion in MapToStruct function.
2026-01-13 12:18:13 +02:00
Hein
0ac207d80f fix: better update handling 2026-01-13 11:33:45 +02:00
Hein
b7a67a6974 fix(headers): 🐛 handle search on computed columns
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -27m24s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -26m56s
Build , Vet Test, and Lint / Build (push) Successful in -27m1s
Build , Vet Test, and Lint / Lint Code (push) Successful in -26m32s
Tests / Integration Tests (push) Failing after -27m44s
Tests / Unit Tests (push) Successful in -27m26s
2026-01-12 11:12:42 +02:00
Hein
cb20a354fc feat(cors): update SetCORSHeaders to accept Request
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -27m41s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 53s
Tests / Unit Tests (push) Successful in 29s
Build , Vet Test, and Lint / Lint Code (push) Successful in -27m8s
Build , Vet Test, and Lint / Build (push) Successful in -27m25s
Tests / Integration Tests (push) Failing after 37s
* Modify SetCORSHeaders function to include Request parameter.
* Set Access-Control-Allow-Origin and Access-Control-Allow-Headers to "*".
* Update all relevant calls to SetCORSHeaders across the codebase.
2026-01-07 15:24:44 +02:00
Hein
37c85361ba feat(cors): add check for server port in CORS config
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -27m37s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 55s
Build , Vet Test, and Lint / Lint Code (push) Successful in -27m0s
Build , Vet Test, and Lint / Build (push) Successful in -27m20s
Tests / Integration Tests (push) Failing after -27m54s
Tests / Unit Tests (push) Successful in 2m1s
2026-01-07 12:06:08 +02:00
14 changed files with 1356 additions and 193 deletions

View File

@@ -26,10 +26,13 @@ func DefaultCORSConfig() CORSConfig {
for i := range cfg.Servers.Instances { for i := range cfg.Servers.Instances {
server := cfg.Servers.Instances[i] server := cfg.Servers.Instances[i]
if server.Port == 0 {
continue
}
hosts = append(hosts, server.ExternalURLs...)
hosts = append(hosts, fmt.Sprintf("http://%s:%d", server.Host, server.Port)) 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("https://%s:%d", server.Host, server.Port))
hosts = append(hosts, fmt.Sprintf("http://%s:%d", "localhost", server.Port)) hosts = append(hosts, fmt.Sprintf("http://%s:%d", "localhost", server.Port))
hosts = append(hosts, server.ExternalURLs...)
for _, ip := range ipsList { for _, ip := range ipsList {
hosts = append(hosts, fmt.Sprintf("http://%s:%d", ip.String(), server.Port)) hosts = append(hosts, fmt.Sprintf("http://%s:%d", ip.String(), server.Port))
hosts = append(hosts, fmt.Sprintf("https://%s:%d", ip.String(), server.Port)) hosts = append(hosts, fmt.Sprintf("https://%s:%d", ip.String(), server.Port))
@@ -111,11 +114,14 @@ func GetHeadSpecHeaders() []string {
} }
// SetCORSHeaders sets CORS headers on a response writer // SetCORSHeaders sets CORS headers on a response writer
func SetCORSHeaders(w ResponseWriter, config CORSConfig) { func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
// Set allowed origins // Set allowed origins
if len(config.AllowedOrigins) > 0 { // if len(config.AllowedOrigins) > 0 {
w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", ")) // w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", "))
} // }
// Todo origin list parsing
w.SetHeader("Access-Control-Allow-Origin", "*")
// Set allowed methods // Set allowed methods
if len(config.AllowedMethods) > 0 { if len(config.AllowedMethods) > 0 {
@@ -123,9 +129,10 @@ func SetCORSHeaders(w ResponseWriter, config CORSConfig) {
} }
// Set allowed headers // Set allowed headers
if len(config.AllowedHeaders) > 0 { // if len(config.AllowedHeaders) > 0 {
w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) // w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
} // }
w.SetHeader("Access-Control-Allow-Headers", "*")
// Set max age // Set max age
if config.MaxAge > 0 { if config.MaxAge > 0 {
@@ -136,5 +143,7 @@ func SetCORSHeaders(w ResponseWriter, config CORSConfig) {
w.SetHeader("Access-Control-Allow-Credentials", "true") w.SetHeader("Access-Control-Allow-Credentials", "true")
// Expose headers that clients can read // Expose headers that clients can read
w.SetHeader("Access-Control-Expose-Headers", "Content-Range, X-Api-Range-Total, X-Api-Range-Size") exposeHeaders := config.AllowedHeaders
exposeHeaders = append(exposeHeaders, "Content-Range", "X-Api-Range-Total", "X-Api-Range-Size")
w.SetHeader("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ", "))
} }

View File

@@ -1,6 +1,7 @@
package dbmanager package dbmanager
import ( import (
"database/sql"
"fmt" "fmt"
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers" "github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
@@ -49,3 +50,18 @@ func createProvider(dbType DatabaseType) (Provider, error) {
// Provider is an alias to the providers.Provider interface // Provider is an alias to the providers.Provider interface
// This allows dbmanager package consumers to use Provider without importing providers // This allows dbmanager package consumers to use Provider without importing providers
type Provider = providers.Provider type Provider = providers.Provider
// NewConnectionFromDB creates a new Connection from an existing *sql.DB
// This allows you to use dbmanager features (ORM wrappers, health checks, etc.)
// with a database connection that was opened outside of dbmanager
//
// Parameters:
// - name: A unique name for this connection
// - dbType: The database type (DatabaseTypePostgreSQL, DatabaseTypeSQLite, or DatabaseTypeMSSQL)
// - db: An existing *sql.DB connection
//
// Returns a Connection that wraps the existing *sql.DB
func NewConnectionFromDB(name string, dbType DatabaseType, db *sql.DB) Connection {
provider := providers.NewExistingDBProvider(db, name)
return newSQLConnection(name, dbType, ConnectionConfig{Name: name, Type: dbType}, provider)
}

View File

@@ -0,0 +1,210 @@
package dbmanager
import (
"context"
"database/sql"
"testing"
_ "github.com/mattn/go-sqlite3"
)
func TestNewConnectionFromDB(t *testing.T) {
// Open a SQLite in-memory database
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Create a connection from the existing database
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
if conn == nil {
t.Fatal("Expected connection to be created")
}
// Verify connection properties
if conn.Name() != "test-connection" {
t.Errorf("Expected name 'test-connection', got '%s'", conn.Name())
}
if conn.Type() != DatabaseTypeSQLite {
t.Errorf("Expected type DatabaseTypeSQLite, got '%s'", conn.Type())
}
}
func TestNewConnectionFromDB_Connect(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
// Connect should verify the existing connection works
err = conn.Connect(ctx)
if err != nil {
t.Errorf("Expected Connect to succeed, got error: %v", err)
}
// Cleanup
conn.Close()
}
func TestNewConnectionFromDB_Native(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
err = conn.Connect(ctx)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Get native DB
nativeDB, err := conn.Native()
if err != nil {
t.Errorf("Expected Native to succeed, got error: %v", err)
}
if nativeDB != db {
t.Error("Expected Native to return the same database instance")
}
}
func TestNewConnectionFromDB_Bun(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
err = conn.Connect(ctx)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Get Bun ORM
bunDB, err := conn.Bun()
if err != nil {
t.Errorf("Expected Bun to succeed, got error: %v", err)
}
if bunDB == nil {
t.Error("Expected Bun to return a non-nil instance")
}
}
func TestNewConnectionFromDB_GORM(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
err = conn.Connect(ctx)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Get GORM
gormDB, err := conn.GORM()
if err != nil {
t.Errorf("Expected GORM to succeed, got error: %v", err)
}
if gormDB == nil {
t.Error("Expected GORM to return a non-nil instance")
}
}
func TestNewConnectionFromDB_HealthCheck(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
err = conn.Connect(ctx)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Health check should succeed
err = conn.HealthCheck(ctx)
if err != nil {
t.Errorf("Expected HealthCheck to succeed, got error: %v", err)
}
}
func TestNewConnectionFromDB_Stats(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
ctx := context.Background()
err = conn.Connect(ctx)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
stats := conn.Stats()
if stats == nil {
t.Fatal("Expected stats to be returned")
}
if stats.Name != "test-connection" {
t.Errorf("Expected stats.Name to be 'test-connection', got '%s'", stats.Name)
}
if stats.Type != DatabaseTypeSQLite {
t.Errorf("Expected stats.Type to be DatabaseTypeSQLite, got '%s'", stats.Type)
}
if !stats.Connected {
t.Error("Expected stats.Connected to be true")
}
}
func TestNewConnectionFromDB_PostgreSQL(t *testing.T) {
// This test just verifies the factory works with PostgreSQL type
// It won't actually connect since we're using SQLite
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
conn := NewConnectionFromDB("test-pg", DatabaseTypePostgreSQL, db)
if conn == nil {
t.Fatal("Expected connection to be created")
}
if conn.Type() != DatabaseTypePostgreSQL {
t.Errorf("Expected type DatabaseTypePostgreSQL, got '%s'", conn.Type())
}
}

View File

@@ -0,0 +1,111 @@
package providers
import (
"context"
"database/sql"
"fmt"
"sync"
"go.mongodb.org/mongo-driver/mongo"
)
// ExistingDBProvider wraps an existing *sql.DB connection
// This allows using dbmanager features with a database connection
// that was opened outside of the dbmanager package
type ExistingDBProvider struct {
db *sql.DB
name string
mu sync.RWMutex
}
// NewExistingDBProvider creates a new provider wrapping an existing *sql.DB
func NewExistingDBProvider(db *sql.DB, name string) *ExistingDBProvider {
return &ExistingDBProvider{
db: db,
name: name,
}
}
// Connect verifies the existing database connection is valid
// It does NOT create a new connection, but ensures the existing one works
func (p *ExistingDBProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db == nil {
return fmt.Errorf("database connection is nil")
}
// Verify the connection works
if err := p.db.PingContext(ctx); err != nil {
return fmt.Errorf("failed to ping existing database: %w", err)
}
return nil
}
// Close closes the underlying database connection
func (p *ExistingDBProvider) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db == nil {
return nil
}
return p.db.Close()
}
// HealthCheck verifies the connection is alive
func (p *ExistingDBProvider) HealthCheck(ctx context.Context) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.db == nil {
return fmt.Errorf("database connection is nil")
}
return p.db.PingContext(ctx)
}
// GetNative returns the wrapped *sql.DB
func (p *ExistingDBProvider) GetNative() (*sql.DB, error) {
p.mu.RLock()
defer p.mu.RUnlock()
if p.db == nil {
return nil, fmt.Errorf("database connection is nil")
}
return p.db, nil
}
// GetMongo returns an error since this is a SQL database
func (p *ExistingDBProvider) GetMongo() (*mongo.Client, error) {
return nil, ErrNotMongoDB
}
// Stats returns connection statistics
func (p *ExistingDBProvider) Stats() *ConnectionStats {
p.mu.RLock()
defer p.mu.RUnlock()
stats := &ConnectionStats{
Name: p.name,
Type: "sql", // Generic since we don't know the specific type
Connected: p.db != nil,
}
if p.db != nil {
dbStats := p.db.Stats()
stats.OpenConnections = dbStats.OpenConnections
stats.InUse = dbStats.InUse
stats.Idle = dbStats.Idle
stats.WaitCount = dbStats.WaitCount
stats.WaitDuration = dbStats.WaitDuration
stats.MaxIdleClosed = dbStats.MaxIdleClosed
stats.MaxLifetimeClosed = dbStats.MaxLifetimeClosed
}
return stats
}

View File

@@ -0,0 +1,194 @@
package providers
import (
"context"
"database/sql"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
)
func TestNewExistingDBProvider(t *testing.T) {
// Open a SQLite in-memory database
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Create provider
provider := NewExistingDBProvider(db, "test-db")
if provider == nil {
t.Fatal("Expected provider to be created")
}
if provider.name != "test-db" {
t.Errorf("Expected name 'test-db', got '%s'", provider.name)
}
}
func TestExistingDBProvider_Connect(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
provider := NewExistingDBProvider(db, "test-db")
ctx := context.Background()
// Connect should verify the connection works
err = provider.Connect(ctx, nil)
if err != nil {
t.Errorf("Expected Connect to succeed, got error: %v", err)
}
}
func TestExistingDBProvider_Connect_NilDB(t *testing.T) {
provider := NewExistingDBProvider(nil, "test-db")
ctx := context.Background()
err := provider.Connect(ctx, nil)
if err == nil {
t.Error("Expected Connect to fail with nil database")
}
}
func TestExistingDBProvider_GetNative(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
provider := NewExistingDBProvider(db, "test-db")
nativeDB, err := provider.GetNative()
if err != nil {
t.Errorf("Expected GetNative to succeed, got error: %v", err)
}
if nativeDB != db {
t.Error("Expected GetNative to return the same database instance")
}
}
func TestExistingDBProvider_GetNative_NilDB(t *testing.T) {
provider := NewExistingDBProvider(nil, "test-db")
_, err := provider.GetNative()
if err == nil {
t.Error("Expected GetNative to fail with nil database")
}
}
func TestExistingDBProvider_HealthCheck(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
provider := NewExistingDBProvider(db, "test-db")
ctx := context.Background()
err = provider.HealthCheck(ctx)
if err != nil {
t.Errorf("Expected HealthCheck to succeed, got error: %v", err)
}
}
func TestExistingDBProvider_HealthCheck_ClosedDB(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
provider := NewExistingDBProvider(db, "test-db")
// Close the database
db.Close()
ctx := context.Background()
err = provider.HealthCheck(ctx)
if err == nil {
t.Error("Expected HealthCheck to fail with closed database")
}
}
func TestExistingDBProvider_GetMongo(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
provider := NewExistingDBProvider(db, "test-db")
_, err = provider.GetMongo()
if err != ErrNotMongoDB {
t.Errorf("Expected ErrNotMongoDB, got: %v", err)
}
}
func TestExistingDBProvider_Stats(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Set some connection pool settings to test stats
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
provider := NewExistingDBProvider(db, "test-db")
stats := provider.Stats()
if stats == nil {
t.Fatal("Expected stats to be returned")
}
if stats.Name != "test-db" {
t.Errorf("Expected stats.Name to be 'test-db', got '%s'", stats.Name)
}
if stats.Type != "sql" {
t.Errorf("Expected stats.Type to be 'sql', got '%s'", stats.Type)
}
if !stats.Connected {
t.Error("Expected stats.Connected to be true")
}
}
func TestExistingDBProvider_Close(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
provider := NewExistingDBProvider(db, "test-db")
err = provider.Close()
if err != nil {
t.Errorf("Expected Close to succeed, got error: %v", err)
}
// Verify the database is closed
err = db.Ping()
if err == nil {
t.Error("Expected database to be closed")
}
}
func TestExistingDBProvider_Close_NilDB(t *testing.T) {
provider := NewExistingDBProvider(nil, "test-db")
err := provider.Close()
if err != nil {
t.Errorf("Expected Close to succeed with nil database, got error: %v", err)
}
}

View File

@@ -948,29 +948,35 @@ func MapToStruct(dataMap map[string]interface{}, target interface{}) error {
// Build list of possible column names for this field // Build list of possible column names for this field
var columnNames []string var columnNames []string
// 1. Bun tag // 1. JSON tag (primary - most common)
jsonFound := false
if jsonTag := field.Tag.Get("json"); jsonTag != "" && jsonTag != "-" {
parts := strings.Split(jsonTag, ",")
if len(parts) > 0 && parts[0] != "" {
columnNames = append(columnNames, parts[0])
jsonFound = true
}
}
// 2. Bun tag (fallback if no JSON tag)
if !jsonFound {
if bunTag := field.Tag.Get("bun"); bunTag != "" && bunTag != "-" { if bunTag := field.Tag.Get("bun"); bunTag != "" && bunTag != "-" {
if colName := ExtractColumnFromBunTag(bunTag); colName != "" { if colName := ExtractColumnFromBunTag(bunTag); colName != "" {
columnNames = append(columnNames, colName) columnNames = append(columnNames, colName)
} }
} }
}
// 2. Gorm tag // 3. Gorm tag (fallback if no JSON tag)
if !jsonFound {
if gormTag := field.Tag.Get("gorm"); gormTag != "" && gormTag != "-" { if gormTag := field.Tag.Get("gorm"); gormTag != "" && gormTag != "-" {
if colName := ExtractColumnFromGormTag(gormTag); colName != "" { if colName := ExtractColumnFromGormTag(gormTag); colName != "" {
columnNames = append(columnNames, colName) columnNames = append(columnNames, colName)
} }
} }
// 3. JSON tag
if jsonTag := field.Tag.Get("json"); jsonTag != "" && jsonTag != "-" {
parts := strings.Split(jsonTag, ",")
if len(parts) > 0 && parts[0] != "" {
columnNames = append(columnNames, parts[0])
}
} }
// 4. Field name variations // 4. Field name variations (last resort)
columnNames = append(columnNames, field.Name) columnNames = append(columnNames, field.Name)
columnNames = append(columnNames, strings.ToLower(field.Name)) columnNames = append(columnNames, strings.ToLower(field.Name))
// columnNames = append(columnNames, ToSnakeCase(field.Name)) // columnNames = append(columnNames, ToSnakeCase(field.Name))
@@ -1096,6 +1102,12 @@ func setFieldValue(field reflect.Value, value interface{}) error {
} }
} }
// If we can convert the type, do it
if valueReflect.Type().ConvertibleTo(field.Type()) {
field.Set(valueReflect.Convert(field.Type()))
return nil
}
// Handle struct types (like SqlTimeStamp, SqlDate, SqlTime which wrap SqlNull[time.Time]) // Handle struct types (like SqlTimeStamp, SqlDate, SqlTime which wrap SqlNull[time.Time])
if field.Kind() == reflect.Struct { if field.Kind() == reflect.Struct {
@@ -1107,9 +1119,9 @@ func setFieldValue(field reflect.Value, value interface{}) error {
// Call the Scan method with the value // Call the Scan method with the value
results := scanMethod.Call([]reflect.Value{reflect.ValueOf(value)}) results := scanMethod.Call([]reflect.Value{reflect.ValueOf(value)})
if len(results) > 0 { if len(results) > 0 {
// Check if there was an error // The Scan method returns error - check if it's nil
if err, ok := results[0].Interface().(error); ok && err != nil { if !results[0].IsNil() {
return err return results[0].Interface().(error)
} }
return nil return nil
} }
@@ -1164,12 +1176,6 @@ func setFieldValue(field reflect.Value, value interface{}) error {
} }
// If we can convert the type, do it
if valueReflect.Type().ConvertibleTo(field.Type()) {
field.Set(valueReflect.Convert(field.Type()))
return nil
}
return fmt.Errorf("cannot convert %v to %v", valueReflect.Type(), field.Type()) return fmt.Errorf("cannot convert %v to %v", valueReflect.Type(), field.Type())
} }

View File

@@ -0,0 +1,120 @@
package reflection_test
import (
"database/sql"
"testing"
"time"
"github.com/bitechdev/ResolveSpec/pkg/reflection"
)
func TestMapToStruct_StandardSqlNullTypes(t *testing.T) {
// Test model with standard library sql.Null* types
type TestModel struct {
ID int64 `bun:"id,pk" json:"id"`
Age sql.NullInt64 `bun:"age" json:"age"`
Name sql.NullString `bun:"name" json:"name"`
Score sql.NullFloat64 `bun:"score" json:"score"`
Active sql.NullBool `bun:"active" json:"active"`
UpdatedAt sql.NullTime `bun:"updated_at" json:"updated_at"`
}
now := time.Now()
dataMap := map[string]any{
"id": int64(100),
"age": int64(25),
"name": "John Doe",
"score": 95.5,
"active": true,
"updated_at": now,
}
var result TestModel
err := reflection.MapToStruct(dataMap, &result)
if err != nil {
t.Fatalf("MapToStruct() error = %v", err)
}
// Verify ID
if result.ID != 100 {
t.Errorf("ID = %v, want 100", result.ID)
}
// Verify Age (sql.NullInt64)
if !result.Age.Valid {
t.Error("Age.Valid = false, want true")
}
if result.Age.Int64 != 25 {
t.Errorf("Age.Int64 = %v, want 25", result.Age.Int64)
}
// Verify Name (sql.NullString)
if !result.Name.Valid {
t.Error("Name.Valid = false, want true")
}
if result.Name.String != "John Doe" {
t.Errorf("Name.String = %v, want 'John Doe'", result.Name.String)
}
// Verify Score (sql.NullFloat64)
if !result.Score.Valid {
t.Error("Score.Valid = false, want true")
}
if result.Score.Float64 != 95.5 {
t.Errorf("Score.Float64 = %v, want 95.5", result.Score.Float64)
}
// Verify Active (sql.NullBool)
if !result.Active.Valid {
t.Error("Active.Valid = false, want true")
}
if !result.Active.Bool {
t.Error("Active.Bool = false, want true")
}
// Verify UpdatedAt (sql.NullTime)
if !result.UpdatedAt.Valid {
t.Error("UpdatedAt.Valid = false, want true")
}
if !result.UpdatedAt.Time.Equal(now) {
t.Errorf("UpdatedAt.Time = %v, want %v", result.UpdatedAt.Time, now)
}
t.Log("All standard library sql.Null* types handled correctly!")
}
func TestMapToStruct_StandardSqlNullTypes_WithNil(t *testing.T) {
// Test nil handling for standard library sql.Null* types
type TestModel struct {
ID int64 `bun:"id,pk" json:"id"`
Age sql.NullInt64 `bun:"age" json:"age"`
Name sql.NullString `bun:"name" json:"name"`
}
dataMap := map[string]any{
"id": int64(200),
"age": int64(30),
"name": nil, // Explicitly nil
}
var result TestModel
err := reflection.MapToStruct(dataMap, &result)
if err != nil {
t.Fatalf("MapToStruct() error = %v", err)
}
// Age should be valid
if !result.Age.Valid {
t.Error("Age.Valid = false, want true")
}
if result.Age.Int64 != 30 {
t.Errorf("Age.Int64 = %v, want 30", result.Age.Int64)
}
// Name should be invalid (null)
if result.Name.Valid {
t.Error("Name.Valid = true, want false (null)")
}
t.Log("Nil handling for sql.Null* types works correctly!")
}

View File

@@ -0,0 +1,364 @@
package reflection
import (
"testing"
"time"
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
"github.com/google/uuid"
)
// TestModel contains all spectypes custom types
type TestModel struct {
ID int64 `bun:"id,pk" json:"id"`
Name spectypes.SqlString `bun:"name" json:"name"`
Age spectypes.SqlInt64 `bun:"age" json:"age"`
Score spectypes.SqlFloat64 `bun:"score" json:"score"`
Active spectypes.SqlBool `bun:"active" json:"active"`
UUID spectypes.SqlUUID `bun:"uuid" json:"uuid"`
CreatedAt spectypes.SqlTimeStamp `bun:"created_at" json:"created_at"`
BirthDate spectypes.SqlDate `bun:"birth_date" json:"birth_date"`
StartTime spectypes.SqlTime `bun:"start_time" json:"start_time"`
Metadata spectypes.SqlJSONB `bun:"metadata" json:"metadata"`
Count16 spectypes.SqlInt16 `bun:"count16" json:"count16"`
Count32 spectypes.SqlInt32 `bun:"count32" json:"count32"`
}
// TestMapToStruct_AllSpectypes verifies that MapToStruct can convert all spectypes correctly
func TestMapToStruct_AllSpectypes(t *testing.T) {
testUUID := uuid.New()
testTime := time.Now()
tests := []struct {
name string
dataMap map[string]interface{}
validator func(*testing.T, *TestModel)
}{
{
name: "SqlString from string",
dataMap: map[string]interface{}{
"name": "John Doe",
},
validator: func(t *testing.T, m *TestModel) {
if !m.Name.Valid || m.Name.String() != "John Doe" {
t.Errorf("expected name='John Doe', got valid=%v, value=%s", m.Name.Valid, m.Name.String())
}
},
},
{
name: "SqlInt64 from int64",
dataMap: map[string]interface{}{
"age": int64(42),
},
validator: func(t *testing.T, m *TestModel) {
if !m.Age.Valid || m.Age.Int64() != 42 {
t.Errorf("expected age=42, got valid=%v, value=%d", m.Age.Valid, m.Age.Int64())
}
},
},
{
name: "SqlInt64 from string",
dataMap: map[string]interface{}{
"age": "99",
},
validator: func(t *testing.T, m *TestModel) {
if !m.Age.Valid || m.Age.Int64() != 99 {
t.Errorf("expected age=99, got valid=%v, value=%d", m.Age.Valid, m.Age.Int64())
}
},
},
{
name: "SqlFloat64 from float64",
dataMap: map[string]interface{}{
"score": float64(98.5),
},
validator: func(t *testing.T, m *TestModel) {
if !m.Score.Valid || m.Score.Float64() != 98.5 {
t.Errorf("expected score=98.5, got valid=%v, value=%f", m.Score.Valid, m.Score.Float64())
}
},
},
{
name: "SqlBool from bool",
dataMap: map[string]interface{}{
"active": true,
},
validator: func(t *testing.T, m *TestModel) {
if !m.Active.Valid || !m.Active.Bool() {
t.Errorf("expected active=true, got valid=%v, value=%v", m.Active.Valid, m.Active.Bool())
}
},
},
{
name: "SqlUUID from string",
dataMap: map[string]interface{}{
"uuid": testUUID.String(),
},
validator: func(t *testing.T, m *TestModel) {
if !m.UUID.Valid || m.UUID.UUID() != testUUID {
t.Errorf("expected uuid=%s, got valid=%v, value=%s", testUUID.String(), m.UUID.Valid, m.UUID.UUID().String())
}
},
},
{
name: "SqlTimeStamp from time.Time",
dataMap: map[string]interface{}{
"created_at": testTime,
},
validator: func(t *testing.T, m *TestModel) {
if !m.CreatedAt.Valid {
t.Errorf("expected created_at to be valid")
}
// Check if times are close enough (within a second)
diff := m.CreatedAt.Time().Sub(testTime)
if diff < -time.Second || diff > time.Second {
t.Errorf("time difference too large: %v", diff)
}
},
},
{
name: "SqlTimeStamp from string",
dataMap: map[string]interface{}{
"created_at": "2024-01-15T10:30:00",
},
validator: func(t *testing.T, m *TestModel) {
if !m.CreatedAt.Valid {
t.Errorf("expected created_at to be valid")
}
expected := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
if m.CreatedAt.Time().Year() != expected.Year() ||
m.CreatedAt.Time().Month() != expected.Month() ||
m.CreatedAt.Time().Day() != expected.Day() {
t.Errorf("expected date 2024-01-15, got %v", m.CreatedAt.Time())
}
},
},
{
name: "SqlDate from string",
dataMap: map[string]interface{}{
"birth_date": "2000-05-20",
},
validator: func(t *testing.T, m *TestModel) {
if !m.BirthDate.Valid {
t.Errorf("expected birth_date to be valid")
}
expected := "2000-05-20"
if m.BirthDate.String() != expected {
t.Errorf("expected date=%s, got %s", expected, m.BirthDate.String())
}
},
},
{
name: "SqlTime from string",
dataMap: map[string]interface{}{
"start_time": "14:30:00",
},
validator: func(t *testing.T, m *TestModel) {
if !m.StartTime.Valid {
t.Errorf("expected start_time to be valid")
}
if m.StartTime.String() != "14:30:00" {
t.Errorf("expected time=14:30:00, got %s", m.StartTime.String())
}
},
},
{
name: "SqlJSONB from map",
dataMap: map[string]interface{}{
"metadata": map[string]interface{}{
"key1": "value1",
"key2": 123,
},
},
validator: func(t *testing.T, m *TestModel) {
if len(m.Metadata) == 0 {
t.Errorf("expected metadata to have data")
}
asMap, err := m.Metadata.AsMap()
if err != nil {
t.Fatalf("failed to convert metadata to map: %v", err)
}
if asMap["key1"] != "value1" {
t.Errorf("expected key1=value1, got %v", asMap["key1"])
}
},
},
{
name: "SqlJSONB from string",
dataMap: map[string]interface{}{
"metadata": `{"test":"data"}`,
},
validator: func(t *testing.T, m *TestModel) {
if len(m.Metadata) == 0 {
t.Errorf("expected metadata to have data")
}
asMap, err := m.Metadata.AsMap()
if err != nil {
t.Fatalf("failed to convert metadata to map: %v", err)
}
if asMap["test"] != "data" {
t.Errorf("expected test=data, got %v", asMap["test"])
}
},
},
{
name: "SqlJSONB from []byte",
dataMap: map[string]interface{}{
"metadata": []byte(`{"byte":"array"}`),
},
validator: func(t *testing.T, m *TestModel) {
if len(m.Metadata) == 0 {
t.Errorf("expected metadata to have data")
}
if string(m.Metadata) != `{"byte":"array"}` {
t.Errorf("expected {\"byte\":\"array\"}, got %s", string(m.Metadata))
}
},
},
{
name: "SqlInt16 from int16",
dataMap: map[string]interface{}{
"count16": int16(100),
},
validator: func(t *testing.T, m *TestModel) {
if !m.Count16.Valid || m.Count16.Int64() != 100 {
t.Errorf("expected count16=100, got valid=%v, value=%d", m.Count16.Valid, m.Count16.Int64())
}
},
},
{
name: "SqlInt32 from int32",
dataMap: map[string]interface{}{
"count32": int32(5000),
},
validator: func(t *testing.T, m *TestModel) {
if !m.Count32.Valid || m.Count32.Int64() != 5000 {
t.Errorf("expected count32=5000, got valid=%v, value=%d", m.Count32.Valid, m.Count32.Int64())
}
},
},
{
name: "nil values create invalid nulls",
dataMap: map[string]interface{}{
"name": nil,
"age": nil,
"active": nil,
"created_at": nil,
},
validator: func(t *testing.T, m *TestModel) {
if m.Name.Valid {
t.Error("expected name to be invalid for nil value")
}
if m.Age.Valid {
t.Error("expected age to be invalid for nil value")
}
if m.Active.Valid {
t.Error("expected active to be invalid for nil value")
}
if m.CreatedAt.Valid {
t.Error("expected created_at to be invalid for nil value")
}
},
},
{
name: "all types together",
dataMap: map[string]interface{}{
"id": int64(1),
"name": "Test User",
"age": int64(30),
"score": float64(95.7),
"active": true,
"uuid": testUUID.String(),
"created_at": "2024-01-15T10:30:00",
"birth_date": "1994-06-15",
"start_time": "09:00:00",
"metadata": map[string]interface{}{"role": "admin"},
"count16": int16(50),
"count32": int32(1000),
},
validator: func(t *testing.T, m *TestModel) {
if m.ID != 1 {
t.Errorf("expected id=1, got %d", m.ID)
}
if !m.Name.Valid || m.Name.String() != "Test User" {
t.Errorf("expected name='Test User', got valid=%v, value=%s", m.Name.Valid, m.Name.String())
}
if !m.Age.Valid || m.Age.Int64() != 30 {
t.Errorf("expected age=30, got valid=%v, value=%d", m.Age.Valid, m.Age.Int64())
}
if !m.Score.Valid || m.Score.Float64() != 95.7 {
t.Errorf("expected score=95.7, got valid=%v, value=%f", m.Score.Valid, m.Score.Float64())
}
if !m.Active.Valid || !m.Active.Bool() {
t.Errorf("expected active=true, got valid=%v, value=%v", m.Active.Valid, m.Active.Bool())
}
if !m.UUID.Valid {
t.Error("expected uuid to be valid")
}
if !m.CreatedAt.Valid {
t.Error("expected created_at to be valid")
}
if !m.BirthDate.Valid || m.BirthDate.String() != "1994-06-15" {
t.Errorf("expected birth_date=1994-06-15, got valid=%v, value=%s", m.BirthDate.Valid, m.BirthDate.String())
}
if !m.StartTime.Valid || m.StartTime.String() != "09:00:00" {
t.Errorf("expected start_time=09:00:00, got valid=%v, value=%s", m.StartTime.Valid, m.StartTime.String())
}
if len(m.Metadata) == 0 {
t.Error("expected metadata to have data")
}
if !m.Count16.Valid || m.Count16.Int64() != 50 {
t.Errorf("expected count16=50, got valid=%v, value=%d", m.Count16.Valid, m.Count16.Int64())
}
if !m.Count32.Valid || m.Count32.Int64() != 1000 {
t.Errorf("expected count32=1000, got valid=%v, value=%d", m.Count32.Valid, m.Count32.Int64())
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
model := &TestModel{}
if err := MapToStruct(tt.dataMap, model); err != nil {
t.Fatalf("MapToStruct failed: %v", err)
}
tt.validator(t, model)
})
}
}
// TestMapToStruct_PartialUpdate tests that partial updates preserve unset fields
func TestMapToStruct_PartialUpdate(t *testing.T) {
// Create initial model with some values
initial := &TestModel{
ID: 1,
Name: spectypes.NewSqlString("Original Name"),
Age: spectypes.NewSqlInt64(25),
}
// Update only the age field
partialUpdate := map[string]interface{}{
"age": int64(30),
}
// Apply partial update
if err := MapToStruct(partialUpdate, initial); err != nil {
t.Fatalf("MapToStruct failed: %v", err)
}
// Verify age was updated
if !initial.Age.Valid || initial.Age.Int64() != 30 {
t.Errorf("expected age=30, got valid=%v, value=%d", initial.Age.Valid, initial.Age.Int64())
}
// Verify name was preserved (not overwritten with zero value)
if !initial.Name.Valid || initial.Name.String() != "Original Name" {
t.Errorf("expected name='Original Name' to be preserved, got valid=%v, value=%s", initial.Name.Valid, initial.Name.String())
}
// Verify ID was preserved
if initial.ID != 1 {
t.Errorf("expected id=1 to be preserved, got %d", initial.ID)
}
}

View File

@@ -701,9 +701,11 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
// Get the primary key name // Get the primary key name
pkName := reflection.GetPrimaryKeyName(model) pkName := reflection.GetPrimaryKeyName(model)
// Wrap in transaction to ensure BeforeUpdate hook is inside transaction
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// First, read the existing record from the database // First, read the existing record from the database
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := h.db.NewSelect().Model(existingRecord) selectQuery := tx.NewSelect().Model(existingRecord).Column("*")
// Apply conditions to select // Apply conditions to select
if urlID != "" { if urlID != "" {
@@ -724,27 +726,42 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
logger.Warn("No records found to update") return fmt.Errorf("no records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
return
} }
logger.Error("Error fetching existing record: %v", err) return fmt.Errorf("error fetching existing record: %w", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error fetching existing record", err)
return
} }
// Convert existing record to map // Convert existing record to map
existingMap := make(map[string]interface{}) existingMap := make(map[string]interface{})
jsonData, err := json.Marshal(existingRecord) jsonData, err := json.Marshal(existingRecord)
if err != nil { if err != nil {
logger.Error("Error marshaling existing record: %v", err) return fmt.Errorf("error marshaling existing record: %w", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error processing existing record", err)
return
} }
if err := json.Unmarshal(jsonData, &existingMap); err != nil { if err := json.Unmarshal(jsonData, &existingMap); err != nil {
logger.Error("Error unmarshaling existing record: %v", err) return fmt.Errorf("error unmarshaling existing record: %w", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error processing existing record", err) }
return
// Execute BeforeUpdate hooks inside transaction
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
Model: model,
Options: options,
ID: urlID,
Data: updates,
Writer: w,
Tx: tx,
}
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
}
// Use potentially modified data from hook context
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
updates = modifiedData
} }
// Merge only non-null and non-empty values from the incoming request into the existing record // Merge only non-null and non-empty values from the incoming request into the existing record
@@ -764,7 +781,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
} }
// Build update query with merged data // Build update query with merged data
query := h.db.NewUpdate().Table(tableName).SetMap(existingMap) query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
// Apply conditions // Apply conditions
if urlID != "" { if urlID != "" {
@@ -780,18 +797,34 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
result, err := query.Exec(ctx) result, err := query.Exec(ctx)
if err != nil { if err != nil {
logger.Error("Update error: %v", err) return fmt.Errorf("error updating record(s): %w", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", err)
return
} }
if result.RowsAffected() == 0 { if result.RowsAffected() == 0 {
logger.Warn("No records found to update") return fmt.Errorf("no records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil) }
// Execute AfterUpdate hooks inside transaction
hookCtx.Result = updates
hookCtx.Error = nil
if err := h.hooks.Execute(AfterUpdate, hookCtx); err != nil {
return fmt.Errorf("AfterUpdate hook failed: %w", err)
}
return nil
})
if err != nil {
logger.Error("Update error: %v", err)
if err.Error() == "no records found to update" {
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", err)
} else {
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", err)
}
return return
} }
logger.Info("Successfully updated %d records", result.RowsAffected()) logger.Info("Successfully updated record(s)")
// Invalidate cache for this table // Invalidate cache for this table
cacheTags := buildCacheTags(schema, tableName) cacheTags := buildCacheTags(schema, tableName)
if err := invalidateCacheForTags(ctx, cacheTags); err != nil { if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
@@ -849,9 +882,11 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range updates { for _, item := range updates {
if itemID, ok := item["id"]; ok { if itemID, ok := item["id"]; ok {
itemIDStr := fmt.Sprintf("%v", itemID)
// First, read the existing record // First, read the existing record
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID) selectQuery := tx.NewSelect().Model(existingRecord).Column("*").Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
continue // Skip if record not found continue // Skip if record not found
@@ -869,6 +904,29 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
return fmt.Errorf("failed to unmarshal existing record: %w", err) return fmt.Errorf("failed to unmarshal existing record: %w", err)
} }
// Execute BeforeUpdate hooks inside transaction
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
Model: model,
Options: options,
ID: itemIDStr,
Data: item,
Writer: w,
Tx: tx,
}
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
}
// Use potentially modified data from hook context
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
item = modifiedData
}
// Merge only non-null and non-empty values // Merge only non-null and non-empty values
for key, newValue := range item { for key, newValue := range item {
if newValue == nil { if newValue == nil {
@@ -884,6 +942,13 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
if _, err := txQuery.Exec(ctx); err != nil { if _, err := txQuery.Exec(ctx); err != nil {
return err return err
} }
// Execute AfterUpdate hooks inside transaction
hookCtx.Result = item
hookCtx.Error = nil
if err := h.hooks.Execute(AfterUpdate, hookCtx); err != nil {
return fmt.Errorf("AfterUpdate hook failed for ID %v: %w", itemID, err)
}
} }
} }
return nil return nil
@@ -957,9 +1022,11 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
for _, item := range updates { for _, item := range updates {
if itemMap, ok := item.(map[string]interface{}); ok { if itemMap, ok := item.(map[string]interface{}); ok {
if itemID, ok := itemMap["id"]; ok { if itemID, ok := itemMap["id"]; ok {
itemIDStr := fmt.Sprintf("%v", itemID)
// First, read the existing record // First, read the existing record
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID) selectQuery := tx.NewSelect().Model(existingRecord).Column("*").Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
continue // Skip if record not found continue // Skip if record not found
@@ -977,6 +1044,29 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
return fmt.Errorf("failed to unmarshal existing record: %w", err) return fmt.Errorf("failed to unmarshal existing record: %w", err)
} }
// Execute BeforeUpdate hooks inside transaction
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
Model: model,
Options: options,
ID: itemIDStr,
Data: itemMap,
Writer: w,
Tx: tx,
}
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
}
// Use potentially modified data from hook context
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
itemMap = modifiedData
}
// Merge only non-null and non-empty values // Merge only non-null and non-empty values
for key, newValue := range itemMap { for key, newValue := range itemMap {
if newValue == nil { if newValue == nil {
@@ -992,6 +1082,14 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
if _, err := txQuery.Exec(ctx); err != nil { if _, err := txQuery.Exec(ctx); err != nil {
return err return err
} }
// Execute AfterUpdate hooks inside transaction
hookCtx.Result = itemMap
hookCtx.Error = nil
if err := h.hooks.Execute(AfterUpdate, hookCtx); err != nil {
return fmt.Errorf("AfterUpdate hook failed for ID %v: %w", itemID, err)
}
list = append(list, item) list = append(list, item)
} }
} }

View File

@@ -50,8 +50,9 @@ func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware Midd
openAPIHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { openAPIHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
reqAdapter := router.NewHTTPRequest(r) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
handler.HandleOpenAPI(respAdapter, reqAdapter) handler.HandleOpenAPI(respAdapter, reqAdapter)
}) })
muxRouter.Handle("/openapi", openAPIHandler).Methods("GET", "OPTIONS") muxRouter.Handle("/openapi", openAPIHandler).Methods("GET", "OPTIONS")
@@ -98,7 +99,8 @@ func createMuxHandler(handler *Handler, schema, entity, idParam string) http.Han
// Set CORS headers // Set CORS headers
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
@@ -106,7 +108,7 @@ func createMuxHandler(handler *Handler, schema, entity, idParam string) http.Han
if idParam != "" { if idParam != "" {
vars["id"] = mux.Vars(r)[idParam] vars["id"] = mux.Vars(r)[idParam]
} }
reqAdapter := router.NewHTTPRequest(r)
handler.Handle(respAdapter, reqAdapter, vars) handler.Handle(respAdapter, reqAdapter, vars)
} }
} }
@@ -117,7 +119,8 @@ func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.
// Set CORS headers // Set CORS headers
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
@@ -125,7 +128,7 @@ func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.
if idParam != "" { if idParam != "" {
vars["id"] = mux.Vars(r)[idParam] vars["id"] = mux.Vars(r)[idParam]
} }
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars) handler.HandleGet(respAdapter, reqAdapter, vars)
} }
} }
@@ -137,13 +140,14 @@ func createMuxOptionsHandler(handler *Handler, schema, entity string, allowedMet
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
corsConfig.AllowedMethods = allowedMethods corsConfig.AllowedMethods = allowedMethods
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
// Return metadata in the OPTIONS response body // Return metadata in the OPTIONS response body
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
vars["entity"] = entity vars["entity"] = entity
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars) handler.HandleGet(respAdapter, reqAdapter, vars)
} }
} }
@@ -222,15 +226,16 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// Add global /openapi route // Add global /openapi route
r.Handle("GET", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
reqAdapter := router.NewHTTPRequest(req.Request) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
handler.HandleOpenAPI(respAdapter, reqAdapter) handler.HandleOpenAPI(respAdapter, reqAdapter)
return nil return nil
}) })
r.Handle("OPTIONS", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("OPTIONS", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
return nil return nil
}) })
@@ -253,12 +258,13 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// POST route without ID // POST route without ID
r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -266,13 +272,14 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// POST route with ID // POST route with ID
r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -280,12 +287,13 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// GET route without ID // GET route without ID
r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -293,13 +301,14 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// GET route with ID // GET route with ID
r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(req.Request)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -307,14 +316,15 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// OPTIONS route without ID (returns metadata) // OPTIONS route without ID (returns metadata)
r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
reqAdapter := router.NewHTTPRequest(req.Request)
optionsCorsConfig := corsConfig optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"} optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig) common.SetCORSHeaders(respAdapter, reqAdapter, optionsCorsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -322,14 +332,15 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// OPTIONS route with ID (returns metadata) // OPTIONS route with ID (returns metadata)
r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
reqAdapter := router.NewHTTPRequest(req.Request)
optionsCorsConfig := corsConfig optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"POST", "OPTIONS"} optionsCorsConfig.AllowedMethods = []string{"POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig) common.SetCORSHeaders(respAdapter, reqAdapter, optionsCorsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })

View File

@@ -1110,30 +1110,6 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
logger.Info("Updating record in %s.%s", schema, entity) logger.Info("Updating record in %s.%s", schema, entity)
// Execute BeforeUpdate hooks
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
TableName: tableName,
Tx: h.db,
Model: model,
Options: options,
ID: id,
Data: data,
Writer: w,
}
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
logger.Error("BeforeUpdate hook failed: %v", err)
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return
}
// Use potentially modified data from hook context
data = hookCtx.Data
// Convert data to map // Convert data to map
dataMap, ok := data.(map[string]interface{}) dataMap, ok := data.(map[string]interface{})
if !ok { if !ok {
@@ -1167,6 +1143,9 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// Variable to store the updated record // Variable to store the updated record
var updatedRecord interface{} var updatedRecord interface{}
// Declare hook context to be used inside and outside transaction
var hookCtx *HookContext
// Process nested relations if present // Process nested relations if present
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Create temporary nested processor with transaction // Create temporary nested processor with transaction
@@ -1174,7 +1153,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// First, read the existing record from the database // First, read the existing record from the database
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID) selectQuery := tx.NewSelect().Model(existingRecord).Column("*").Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return fmt.Errorf("record not found with ID: %v", targetID) return fmt.Errorf("record not found with ID: %v", targetID)
@@ -1204,6 +1183,30 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
nestedRelations = relations nestedRelations = relations
} }
// Execute BeforeUpdate hooks inside transaction
hookCtx = &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
TableName: tableName,
Tx: tx,
Model: model,
Options: options,
ID: id,
Data: dataMap,
Writer: w,
}
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
}
// Use potentially modified data from hook context
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
dataMap = modifiedData
}
// Merge only non-null and non-empty values from the incoming request into the existing record // Merge only non-null and non-empty values from the incoming request into the existing record
for key, newValue := range dataMap { for key, newValue := range dataMap {
// Skip if the value is nil // Skip if the value is nil

View File

@@ -354,6 +354,12 @@ func (h *Handler) parseSearchOp(options *ExtendedRequestOptions, headerKey, valu
operator := parts[0] operator := parts[0]
colName := parts[1] colName := parts[1]
if strings.HasPrefix(colName, "cql") {
// Computed column - Will not filter on it
logger.Warn("Search operators on computed columns are not supported: %s", colName)
return
}
// Map operator names to filter operators // Map operator names to filter operators
filterOp := h.mapSearchOperator(colName, operator, value) filterOp := h.mapSearchOperator(colName, operator, value)

View File

@@ -103,8 +103,9 @@ func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware Midd
openAPIHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { openAPIHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
reqAdapter := router.NewHTTPRequest(r) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
handler.HandleOpenAPI(respAdapter, reqAdapter) handler.HandleOpenAPI(respAdapter, reqAdapter)
}) })
muxRouter.Handle("/openapi", openAPIHandler).Methods("GET", "OPTIONS") muxRouter.Handle("/openapi", openAPIHandler).Methods("GET", "OPTIONS")
@@ -161,7 +162,8 @@ func createMuxHandler(handler *Handler, schema, entity, idParam string) http.Han
// Set CORS headers // Set CORS headers
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
@@ -169,7 +171,7 @@ func createMuxHandler(handler *Handler, schema, entity, idParam string) http.Han
if idParam != "" { if idParam != "" {
vars["id"] = mux.Vars(r)[idParam] vars["id"] = mux.Vars(r)[idParam]
} }
reqAdapter := router.NewHTTPRequest(r)
handler.Handle(respAdapter, reqAdapter, vars) handler.Handle(respAdapter, reqAdapter, vars)
} }
} }
@@ -180,7 +182,8 @@ func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.
// Set CORS headers // Set CORS headers
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
@@ -188,7 +191,7 @@ func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.
if idParam != "" { if idParam != "" {
vars["id"] = mux.Vars(r)[idParam] vars["id"] = mux.Vars(r)[idParam]
} }
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars) handler.HandleGet(respAdapter, reqAdapter, vars)
} }
} }
@@ -200,13 +203,14 @@ func createMuxOptionsHandler(handler *Handler, schema, entity string, allowedMet
corsConfig := common.DefaultCORSConfig() corsConfig := common.DefaultCORSConfig()
corsConfig.AllowedMethods = allowedMethods corsConfig.AllowedMethods = allowedMethods
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewHTTPRequest(r)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
// Return metadata in the OPTIONS response body // Return metadata in the OPTIONS response body
vars := make(map[string]string) vars := make(map[string]string)
vars["schema"] = schema vars["schema"] = schema
vars["entity"] = entity vars["entity"] = entity
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars) handler.HandleGet(respAdapter, reqAdapter, vars)
} }
} }
@@ -285,15 +289,8 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// Add global /openapi route // Add global /openapi route
r.Handle("GET", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
reqAdapter := router.NewBunRouterRequest(req) reqAdapter := router.NewBunRouterRequest(req)
handler.HandleOpenAPI(respAdapter, reqAdapter) common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
return nil
})
r.Handle("OPTIONS", "/openapi", func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
return nil return nil
}) })
@@ -317,24 +314,26 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// GET and POST for /{schema}/{entity} // GET and POST for /{schema}/{entity}
r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -342,65 +341,70 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}/:id // GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}/:id
r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
r.Handle("PUT", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("PUT", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
r.Handle("PATCH", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("PATCH", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
r.Handle("DELETE", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("DELETE", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
"id": req.Param("id"), "id": req.Param("id"),
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -408,12 +412,13 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// Metadata endpoint // Metadata endpoint
r.Handle("GET", metadataPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", metadataPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig) reqAdapter := router.NewBunRouterRequest(req)
common.SetCORSHeaders(respAdapter, reqAdapter, corsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -421,14 +426,15 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// OPTIONS route without ID (returns metadata) // OPTIONS route without ID (returns metadata)
r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
reqAdapter := router.NewBunRouterRequest(req)
optionsCorsConfig := corsConfig optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"} optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig) common.SetCORSHeaders(respAdapter, reqAdapter, optionsCorsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })
@@ -436,14 +442,15 @@ func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// OPTIONS route with ID (returns metadata) // OPTIONS route with ID (returns metadata)
r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w) respAdapter := router.NewHTTPResponseWriter(w)
reqAdapter := router.NewBunRouterRequest(req)
optionsCorsConfig := corsConfig optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "PUT", "PATCH", "DELETE", "POST", "OPTIONS"} optionsCorsConfig.AllowedMethods = []string{"GET", "PUT", "PATCH", "DELETE", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig) common.SetCORSHeaders(respAdapter, reqAdapter, optionsCorsConfig)
params := map[string]string{ params := map[string]string{
"schema": currentSchema, "schema": currentSchema,
"entity": currentEntity, "entity": currentEntity,
} }
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params) handler.HandleGet(respAdapter, reqAdapter, params)
return nil return nil
}) })

View File

@@ -74,6 +74,10 @@ func (n *SqlNull[T]) Scan(value any) error {
return n.FromString(v) return n.FromString(v)
case []byte: case []byte:
return n.FromString(string(v)) return n.FromString(string(v))
case float32, float64:
return n.FromString(fmt.Sprintf("%f", value))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return n.FromString(fmt.Sprintf("%d", value))
default: default:
return n.FromString(fmt.Sprintf("%v", value)) return n.FromString(fmt.Sprintf("%v", value))
} }
@@ -94,6 +98,10 @@ func (n *SqlNull[T]) FromString(s string) error {
reflect.ValueOf(&n.Val).Elem().SetInt(i) reflect.ValueOf(&n.Val).Elem().SetInt(i)
n.Valid = true n.Valid = true
} }
if f, err := strconv.ParseFloat(s, 64); err == nil {
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
n.Valid = true
}
case float32, float64: case float32, float64:
if f, err := strconv.ParseFloat(s, 64); err == nil { if f, err := strconv.ParseFloat(s, 64); err == nil {
reflect.ValueOf(&n.Val).Elem().SetFloat(f) reflect.ValueOf(&n.Val).Elem().SetFloat(f)