mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-29 07:44:25 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b2d05465e | ||
|
|
e88018543e | ||
|
|
e7e5754a47 |
138
SCHEMA_TABLE_HANDLING.md
Normal file
138
SCHEMA_TABLE_HANDLING.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Schema and Table Name Handling
|
||||
|
||||
This document explains how the handlers properly separate and handle schema and table names.
|
||||
|
||||
## Implementation
|
||||
|
||||
Both `resolvespec` and `restheadspec` handlers now properly handle schema and table name separation through the following functions:
|
||||
|
||||
- `parseTableName(fullTableName)` - Splits "schema.table" into separate components
|
||||
- `getSchemaAndTable(defaultSchema, entity, model)` - Returns schema and table separately
|
||||
- `getTableName(schema, entity, model)` - Returns the full "schema.table" format
|
||||
|
||||
## Priority Order
|
||||
|
||||
When determining the schema and table name, the following priority is used:
|
||||
|
||||
1. **If `TableName()` contains a schema** (e.g., "myschema.mytable"), that schema takes precedence
|
||||
2. **If model implements `SchemaProvider`**, use that schema
|
||||
3. **Otherwise**, use the `defaultSchema` parameter from the URL/request
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Scenario 1: Simple table name, default schema
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
```
|
||||
- Request URL: `/api/public/users`
|
||||
- Result: `schema="public"`, `table="users"`, `fullName="public.users"`
|
||||
|
||||
### Scenario 2: Table name includes schema
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "auth.users" // Schema included!
|
||||
}
|
||||
```
|
||||
- Request URL: `/api/public/users` (public is ignored)
|
||||
- Result: `schema="auth"`, `table="users"`, `fullName="auth.users"`
|
||||
- **Note**: The schema from `TableName()` takes precedence over the URL schema
|
||||
|
||||
### Scenario 3: Using SchemaProvider
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func (User) SchemaName() string {
|
||||
return "auth"
|
||||
}
|
||||
```
|
||||
- Request URL: `/api/public/users` (public is ignored)
|
||||
- Result: `schema="auth"`, `table="users"`, `fullName="auth.users"`
|
||||
|
||||
### Scenario 4: Table name includes schema AND SchemaProvider
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "core.users" // This wins!
|
||||
}
|
||||
|
||||
func (User) SchemaName() string {
|
||||
return "auth" // This is ignored
|
||||
}
|
||||
```
|
||||
- Request URL: `/api/public/users`
|
||||
- Result: `schema="core"`, `table="users"`, `fullName="core.users"`
|
||||
- **Note**: Schema from `TableName()` takes highest precedence
|
||||
|
||||
### Scenario 5: No providers at all
|
||||
```go
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
// No TableName() or SchemaName()
|
||||
```
|
||||
- Request URL: `/api/public/users`
|
||||
- Result: `schema="public"`, `table="users"`, `fullName="public.users"`
|
||||
- Uses URL schema and entity name
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Automatic detection**: The code automatically detects if `TableName()` includes a schema by checking for "."
|
||||
2. **Backward compatible**: Existing code continues to work
|
||||
3. **Flexible**: Supports multiple ways to specify schema and table
|
||||
4. **Debug logging**: Logs when schema is detected in `TableName()` for debugging
|
||||
|
||||
## Code Locations
|
||||
|
||||
### Handlers
|
||||
- `/pkg/resolvespec/handler.go:472-531`
|
||||
- `/pkg/restheadspec/handler.go:534-593`
|
||||
|
||||
### Database Adapters
|
||||
- `/pkg/common/adapters/database/utils.go` - Shared `parseTableName()` function
|
||||
- `/pkg/common/adapters/database/bun.go` - Bun adapter with separated schema/table
|
||||
- `/pkg/common/adapters/database/gorm.go` - GORM adapter with separated schema/table
|
||||
|
||||
## Adapter Implementation
|
||||
|
||||
Both Bun and GORM adapters now properly separate schema and table name:
|
||||
|
||||
```go
|
||||
// BunSelectQuery/GormSelectQuery now have separated fields:
|
||||
type BunSelectQuery struct {
|
||||
query *bun.SelectQuery
|
||||
schema string // Separated schema name
|
||||
tableName string // Just the table name, without schema
|
||||
tableAlias string
|
||||
}
|
||||
```
|
||||
|
||||
When `Model()` or `Table()` is called:
|
||||
1. The full table name (which may include schema) is parsed
|
||||
2. Schema and table name are stored separately
|
||||
3. When building joins, the already-separated table name is used directly
|
||||
|
||||
This ensures consistent handling of schema-qualified table names throughout the codebase.
|
||||
@@ -78,7 +78,8 @@ func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Databa
|
||||
// BunSelectQuery implements SelectQuery for Bun
|
||||
type BunSelectQuery struct {
|
||||
query *bun.SelectQuery
|
||||
tableName string
|
||||
schema string // Separated schema name
|
||||
tableName string // Just the table name, without schema
|
||||
tableAlias string
|
||||
}
|
||||
|
||||
@@ -87,7 +88,9 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
|
||||
// Try to get table name from model if it implements TableNameProvider
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
b.tableName = provider.TableName()
|
||||
fullTableName := provider.TableName()
|
||||
// Check if the table name contains schema (e.g., "schema.table")
|
||||
b.schema, b.tableName = parseTableName(fullTableName)
|
||||
}
|
||||
|
||||
return b
|
||||
@@ -95,7 +98,8 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
|
||||
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
||||
b.query = b.query.Table(table)
|
||||
b.tableName = table
|
||||
// Check if the table name contains schema (e.g., "schema.table")
|
||||
b.schema, b.tableName = parseTableName(table)
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -128,13 +132,9 @@ func (b *BunSelectQuery) Join(query string, args ...interface{}) common.SelectQu
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||
if prefix == "" && b.tableName != "" {
|
||||
prefix = b.tableName
|
||||
// Extract just the table name if it has schema
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// If prefix is provided, add it as an alias in the join
|
||||
@@ -169,12 +169,9 @@ func (b *BunSelectQuery) LeftJoin(query string, args ...interface{}) common.Sele
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||
if prefix == "" && b.tableName != "" {
|
||||
prefix = b.tableName
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Construct LEFT JOIN with prefix
|
||||
|
||||
@@ -70,7 +70,8 @@ func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Datab
|
||||
// GormSelectQuery implements SelectQuery for GORM
|
||||
type GormSelectQuery struct {
|
||||
db *gorm.DB
|
||||
tableName string
|
||||
schema string // Separated schema name
|
||||
tableName string // Just the table name, without schema
|
||||
tableAlias string
|
||||
}
|
||||
|
||||
@@ -79,7 +80,9 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
|
||||
// Try to get table name from model if it implements TableNameProvider
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
g.tableName = provider.TableName()
|
||||
fullTableName := provider.TableName()
|
||||
// Check if the table name contains schema (e.g., "schema.table")
|
||||
g.schema, g.tableName = parseTableName(fullTableName)
|
||||
}
|
||||
|
||||
return g
|
||||
@@ -87,7 +90,8 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
|
||||
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
||||
g.db = g.db.Table(table)
|
||||
g.tableName = table
|
||||
// Check if the table name contains schema (e.g., "schema.table")
|
||||
g.schema, g.tableName = parseTableName(table)
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -120,13 +124,9 @@ func (g *GormSelectQuery) Join(query string, args ...interface{}) common.SelectQ
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||
if prefix == "" && g.tableName != "" {
|
||||
prefix = g.tableName
|
||||
// Extract just the table name if it has schema
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// If prefix is provided, add it as an alias in the join
|
||||
@@ -161,12 +161,9 @@ func (g *GormSelectQuery) LeftJoin(query string, args ...interface{}) common.Sel
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||
if prefix == "" && g.tableName != "" {
|
||||
prefix = g.tableName
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Construct LEFT JOIN with prefix
|
||||
|
||||
13
pkg/common/adapters/database/utils.go
Normal file
13
pkg/common/adapters/database/utils.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package database
|
||||
|
||||
import "strings"
|
||||
|
||||
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||
// For example: "public.users" -> ("public", "users")
|
||||
// "users" -> ("", "users")
|
||||
func parseTableName(fullTableName string) (schema, table string) {
|
||||
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||
return fullTableName[:idx], fullTableName[idx+1:]
|
||||
}
|
||||
return "", fullTableName
|
||||
}
|
||||
@@ -38,12 +38,28 @@ func (r *DefaultModelRegistry) RegisterModel(name string, model interface{}) err
|
||||
return fmt.Errorf("model cannot be nil")
|
||||
}
|
||||
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
return fmt.Errorf("model must be a non-pointer struct, got pointer to %s", modelType.Elem().Kind())
|
||||
originalType := modelType
|
||||
|
||||
// Unwrap pointers, slices, and arrays to check the underlying type
|
||||
for modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
// Validate that the underlying type is a struct
|
||||
if modelType.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("model must be a struct, got %s", modelType.Kind())
|
||||
return fmt.Errorf("model must be a struct or pointer to struct, got %s", originalType.String())
|
||||
}
|
||||
|
||||
// If a pointer/slice/array was passed, unwrap to the base struct
|
||||
if originalType != modelType {
|
||||
// Create a zero value of the struct type
|
||||
model = reflect.New(modelType).Elem().Interface()
|
||||
}
|
||||
|
||||
// Additional check: ensure model is not a pointer
|
||||
finalType := reflect.TypeOf(model)
|
||||
if finalType.Kind() == reflect.Ptr {
|
||||
return fmt.Errorf("model must be a non-pointer struct, got pointer to %s. Use MyModel{} instead of &MyModel{}", finalType.Elem().Name())
|
||||
}
|
||||
|
||||
r.models[name] = model
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
||||
@@ -26,8 +27,22 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// handlePanic is a helper function to handle panics with stack traces
|
||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||
stack := debug.Stack()
|
||||
logger.Error("Panic in %s: %v\nStack trace:\n%s", method, err, string(stack))
|
||||
h.sendError(w, http.StatusInternalServerError, "internal_error", fmt.Sprintf("Internal server error in %s", method), fmt.Errorf("%v", err))
|
||||
}
|
||||
|
||||
// Handle processes API requests through router-agnostic interface
|
||||
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "Handle", err)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
body, err := r.Body()
|
||||
@@ -58,6 +73,26 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that the model is a struct type (not a slice or pointer to slice)
|
||||
modelType := reflect.TypeOf(model)
|
||||
originalType := modelType
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model for %s.%s must be a struct type, got %v. Please register models as struct types, not slices or pointers to slices.", schema, entity, originalType)
|
||||
h.sendError(w, http.StatusInternalServerError, "invalid_model_type",
|
||||
fmt.Sprintf("Model must be a struct type, got %v. Ensure you register the struct (e.g., ModelCoreAccount{}) not a slice (e.g., []*ModelCoreAccount)", originalType),
|
||||
fmt.Errorf("invalid model type: %v", originalType))
|
||||
return
|
||||
}
|
||||
|
||||
// If the registered model was a pointer or slice, use the unwrapped struct type
|
||||
if originalType != modelType {
|
||||
model = reflect.New(modelType).Elem().Interface()
|
||||
}
|
||||
|
||||
// Create a pointer to the model type for database operations
|
||||
modelPtr := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
@@ -82,6 +117,13 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
|
||||
// HandleGet processes GET requests for metadata
|
||||
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "HandleGet", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := params["schema"]
|
||||
entity := params["entity"]
|
||||
|
||||
@@ -99,11 +141,32 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
||||
}
|
||||
|
||||
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options common.RequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleRead", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
model := GetModel(ctx)
|
||||
modelPtr := GetModelPtr(ctx)
|
||||
|
||||
// Validate and unwrap model type to get base struct
|
||||
modelType := reflect.TypeOf(model)
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model must be a struct type, got %v for %s.%s", modelType, schema, entity)
|
||||
h.sendError(w, http.StatusInternalServerError, "invalid_model", "Model must be a struct type", fmt.Errorf("invalid model type: %v", modelType))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a pointer to the model type for database operations
|
||||
modelPtr := reflect.New(modelType).Interface()
|
||||
|
||||
logger.Info("Reading records from %s.%s", schema, entity)
|
||||
|
||||
@@ -160,8 +223,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
var result interface{}
|
||||
if id != "" {
|
||||
logger.Debug("Querying single record with ID: %s", id)
|
||||
// Create a pointer to the struct type for scanning
|
||||
singleResult := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
// Create a pointer to the struct type for scanning - use modelType which is already unwrapped
|
||||
singleResult := reflect.New(modelType).Interface()
|
||||
query = query.Where("id = ?", id)
|
||||
if err := query.Scan(ctx, singleResult); err != nil {
|
||||
logger.Error("Error querying record: %v", err)
|
||||
@@ -171,8 +234,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
result = singleResult
|
||||
} else {
|
||||
logger.Debug("Querying multiple records")
|
||||
// Create a slice of pointers to the model type
|
||||
sliceType := reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(model)))
|
||||
// Create a slice of pointers to the model type - use modelType which is already unwrapped
|
||||
sliceType := reflect.SliceOf(reflect.PointerTo(modelType))
|
||||
results := reflect.New(sliceType).Interface()
|
||||
|
||||
if err := query.Scan(ctx, results); err != nil {
|
||||
@@ -203,6 +266,13 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, data interface{}, options common.RequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleCreate", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -279,6 +349,13 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleUpdate", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -329,6 +406,13 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
}
|
||||
|
||||
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleDelete", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -385,19 +469,86 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
return provider.TableName()
|
||||
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||
return fullTableName[:idx], fullTableName[idx+1:]
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", schema, entity)
|
||||
return "", fullTableName
|
||||
}
|
||||
|
||||
// getSchemaAndTable returns the schema and table name separately
|
||||
// It checks SchemaProvider and TableNameProvider interfaces and handles cases where
|
||||
// the table name may already include the schema (e.g., "public.users")
|
||||
//
|
||||
// Priority order:
|
||||
// 1. If TableName() contains a schema (e.g., "myschema.mytable"), that schema takes precedence
|
||||
// 2. If model implements SchemaProvider, use that schema
|
||||
// 3. Otherwise, use the defaultSchema parameter
|
||||
func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interface{}) (schema, table string) {
|
||||
// First check if model provides a table name
|
||||
// We check this FIRST because the table name might already contain the schema
|
||||
if tableProvider, ok := model.(common.TableNameProvider); ok {
|
||||
tableName := tableProvider.TableName()
|
||||
|
||||
// IMPORTANT: Check if the table name already contains a schema (e.g., "schema.table")
|
||||
// This is common when models need to specify a different schema than the default
|
||||
if tableSchema, tableOnly := h.parseTableName(tableName); tableSchema != "" {
|
||||
// Table name includes schema - use it and ignore any other schema providers
|
||||
logger.Debug("TableName() includes schema: %s.%s", tableSchema, tableOnly)
|
||||
return tableSchema, tableOnly
|
||||
}
|
||||
|
||||
// Table name is just the table name without schema
|
||||
// Now determine which schema to use
|
||||
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||
schema = schemaProvider.SchemaName()
|
||||
} else {
|
||||
schema = defaultSchema
|
||||
}
|
||||
|
||||
return schema, tableName
|
||||
}
|
||||
|
||||
// No TableNameProvider, so check for schema and use entity as table name
|
||||
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||
schema = schemaProvider.SchemaName()
|
||||
} else {
|
||||
schema = defaultSchema
|
||||
}
|
||||
|
||||
// Default to entity name as table
|
||||
return schema, entity
|
||||
}
|
||||
|
||||
// getTableName returns the full table name including schema (schema.table)
|
||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
schemaName, tableName := h.getSchemaAndTable(schema, entity, model)
|
||||
if schemaName != "" {
|
||||
return fmt.Sprintf("%s.%s", schemaName, tableName)
|
||||
}
|
||||
return tableName
|
||||
}
|
||||
|
||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
|
||||
// Unwrap pointers, slices, and arrays to get to the base struct type
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
// Validate that we have a struct type
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model type must be a struct, got %v for %s.%s", modelType, schema, entity)
|
||||
return &common.TableMetadata{
|
||||
Schema: schema,
|
||||
Table: entity,
|
||||
Columns: make([]common.Column, 0),
|
||||
Relations: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
metadata := &common.TableMetadata{
|
||||
Schema: schema,
|
||||
Table: entity,
|
||||
@@ -541,10 +692,18 @@ type relationshipInfo struct {
|
||||
|
||||
func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, preloads []common.PreloadOption) common.SelectQuery {
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
|
||||
// Unwrap pointers, slices, and arrays to get to the base struct type
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
// Validate that we have a struct type
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Warn("Cannot apply preloads to non-struct type: %v", modelType)
|
||||
return query
|
||||
}
|
||||
|
||||
for _, preload := range preloads {
|
||||
logger.Debug("Processing preload for relation: %s", preload.Relation)
|
||||
relInfo := h.getRelationshipInfo(modelType, preload.Relation)
|
||||
@@ -568,6 +727,12 @@ func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, pre
|
||||
}
|
||||
|
||||
func (h *Handler) getRelationshipInfo(modelType reflect.Type, relationName string) *relationshipInfo {
|
||||
// Ensure we have a struct type
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Warn("Cannot get relationship info from non-struct type: %v", modelType)
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < modelType.NumField(); i++ {
|
||||
field := modelType.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
||||
@@ -27,9 +28,23 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// handlePanic is a helper function to handle panics with stack traces
|
||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||
stack := debug.Stack()
|
||||
logger.Error("Panic in %s: %v\nStack trace:\n%s", method, err, string(stack))
|
||||
h.sendError(w, http.StatusInternalServerError, "internal_error", fmt.Sprintf("Internal server error in %s", method), fmt.Errorf("%v", err))
|
||||
}
|
||||
|
||||
// Handle processes API requests through router-agnostic interface
|
||||
// Options are read from HTTP headers instead of request body
|
||||
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "Handle", err)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
schema := params["schema"]
|
||||
@@ -52,6 +67,26 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that the model is a struct type (not a slice or pointer to slice)
|
||||
modelType := reflect.TypeOf(model)
|
||||
originalType := modelType
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model for %s.%s must be a struct type, got %v. Please register models as struct types, not slices or pointers to slices.", schema, entity, originalType)
|
||||
h.sendError(w, http.StatusInternalServerError, "invalid_model_type",
|
||||
fmt.Sprintf("Model must be a struct type, got %v. Ensure you register the struct (e.g., ModelCoreAccount{}) not a slice (e.g., []*ModelCoreAccount)", originalType),
|
||||
fmt.Errorf("invalid model type: %v", originalType))
|
||||
return
|
||||
}
|
||||
|
||||
// If the registered model was a pointer or slice, use the unwrapped struct type
|
||||
if originalType != modelType {
|
||||
model = reflect.New(modelType).Elem().Interface()
|
||||
}
|
||||
|
||||
modelPtr := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
|
||||
@@ -107,6 +142,13 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
|
||||
// HandleGet processes GET requests for metadata
|
||||
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "HandleGet", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := params["schema"]
|
||||
entity := params["entity"]
|
||||
|
||||
@@ -126,10 +168,32 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
||||
// parseOptionsFromHeaders is now implemented in headers.go
|
||||
|
||||
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options ExtendedRequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleRead", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
modelPtr := GetModelPtr(ctx)
|
||||
model := GetModel(ctx)
|
||||
|
||||
// Validate and unwrap model type to get base struct
|
||||
modelType := reflect.TypeOf(model)
|
||||
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model must be a struct type, got %v for %s.%s", modelType, schema, entity)
|
||||
h.sendError(w, http.StatusInternalServerError, "invalid_model", "Model must be a struct type", fmt.Errorf("invalid model type: %v", modelType))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a pointer to a slice of pointers to the model type for query results
|
||||
modelPtr := reflect.New(reflect.SliceOf(reflect.PointerTo(modelType))).Interface()
|
||||
|
||||
logger.Info("Reading records from %s.%s", schema, entity)
|
||||
|
||||
@@ -223,10 +287,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
query = query.Offset(*options.Offset)
|
||||
}
|
||||
|
||||
// Execute query - create a slice of pointers to the model type
|
||||
model := GetModel(ctx)
|
||||
resultSlice := reflect.New(reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(model)))).Interface()
|
||||
if err := query.Scan(ctx, resultSlice); err != nil {
|
||||
// Execute query - modelPtr was already created earlier
|
||||
if err := query.Scan(ctx, modelPtr); err != nil {
|
||||
logger.Error("Error executing query: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||
return
|
||||
@@ -248,10 +310,17 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
h.sendFormattedResponse(w, resultSlice, metadata, options)
|
||||
h.sendFormattedResponse(w, modelPtr, metadata, options)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, data interface{}, options ExtendedRequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleCreate", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -322,6 +391,13 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id string, idPtr *int64, data interface{}, options ExtendedRequestOptions) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleUpdate", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -369,6 +445,13 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
||||
}
|
||||
|
||||
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) {
|
||||
// Capture panics and return error response
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
h.handlePanic(w, "handleDelete", err)
|
||||
}
|
||||
}()
|
||||
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
@@ -448,28 +531,85 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
// Check if model implements TableNameProvider
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
tableName := provider.TableName()
|
||||
if tableName != "" {
|
||||
return tableName
|
||||
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||
return fullTableName[:idx], fullTableName[idx+1:]
|
||||
}
|
||||
return "", fullTableName
|
||||
}
|
||||
|
||||
// getSchemaAndTable returns the schema and table name separately
|
||||
// It checks SchemaProvider and TableNameProvider interfaces and handles cases where
|
||||
// the table name may already include the schema (e.g., "public.users")
|
||||
//
|
||||
// Priority order:
|
||||
// 1. If TableName() contains a schema (e.g., "myschema.mytable"), that schema takes precedence
|
||||
// 2. If model implements SchemaProvider, use that schema
|
||||
// 3. Otherwise, use the defaultSchema parameter
|
||||
func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interface{}) (schema, table string) {
|
||||
// First check if model provides a table name
|
||||
// We check this FIRST because the table name might already contain the schema
|
||||
if tableProvider, ok := model.(common.TableNameProvider); ok {
|
||||
tableName := tableProvider.TableName()
|
||||
|
||||
// IMPORTANT: Check if the table name already contains a schema (e.g., "schema.table")
|
||||
// This is common when models need to specify a different schema than the default
|
||||
if tableSchema, tableOnly := h.parseTableName(tableName); tableSchema != "" {
|
||||
// Table name includes schema - use it and ignore any other schema providers
|
||||
logger.Debug("TableName() includes schema: %s.%s", tableSchema, tableOnly)
|
||||
return tableSchema, tableOnly
|
||||
}
|
||||
|
||||
// Table name is just the table name without schema
|
||||
// Now determine which schema to use
|
||||
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||
schema = schemaProvider.SchemaName()
|
||||
} else {
|
||||
schema = defaultSchema
|
||||
}
|
||||
|
||||
return schema, tableName
|
||||
}
|
||||
|
||||
// Default to schema.entity
|
||||
if schema != "" {
|
||||
return fmt.Sprintf("%s.%s", schema, entity)
|
||||
// No TableNameProvider, so check for schema and use entity as table name
|
||||
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||
schema = schemaProvider.SchemaName()
|
||||
} else {
|
||||
schema = defaultSchema
|
||||
}
|
||||
return entity
|
||||
|
||||
// Default to entity name as table
|
||||
return schema, entity
|
||||
}
|
||||
|
||||
// getTableName returns the full table name including schema (schema.table)
|
||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
schemaName, tableName := h.getSchemaAndTable(schema, entity, model)
|
||||
if schemaName != "" {
|
||||
return fmt.Sprintf("%s.%s", schemaName, tableName)
|
||||
}
|
||||
return tableName
|
||||
}
|
||||
|
||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
|
||||
// Unwrap pointers, slices, and arrays to get to the base struct type
|
||||
for modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
// Validate that we have a struct type
|
||||
if modelType.Kind() != reflect.Struct {
|
||||
logger.Error("Model type must be a struct, got %s for %s.%s", modelType.Kind(), schema, entity)
|
||||
return &common.TableMetadata{
|
||||
Schema: schema,
|
||||
Table: h.getTableName(schema, entity, model),
|
||||
Columns: []common.Column{},
|
||||
}
|
||||
}
|
||||
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
|
||||
metadata := &common.TableMetadata{
|
||||
|
||||
Reference in New Issue
Block a user