mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 08:44:25 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1adca4c49b | ||
|
|
eefed23766 | ||
|
|
3b2d05465e | ||
|
|
e88018543e | ||
|
|
e7e5754a47 | ||
|
|
c88bff1883 |
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.
|
||||||
@@ -15,12 +15,12 @@ if [[ $make_release =~ ^[Yy]$ ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create an annotated tag
|
# Create an annotated tag
|
||||||
git tag -a "$version" -m "Released Core $version"
|
git tag -a "$version" -m "Released $version"
|
||||||
|
|
||||||
# Push the tag to the remote repository
|
# Push the tag to the remote repository
|
||||||
git push origin "$version"
|
git push origin "$version"
|
||||||
|
|
||||||
echo "Tag $version created for Core and pushed to the remote repository."
|
echo "Tag $version created and pushed to the remote repository."
|
||||||
else
|
else
|
||||||
echo "No release version created."
|
echo "No release version created."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Databa
|
|||||||
// BunSelectQuery implements SelectQuery for Bun
|
// BunSelectQuery implements SelectQuery for Bun
|
||||||
type BunSelectQuery struct {
|
type BunSelectQuery struct {
|
||||||
query *bun.SelectQuery
|
query *bun.SelectQuery
|
||||||
tableName string
|
schema string // Separated schema name
|
||||||
|
tableName string // Just the table name, without schema
|
||||||
tableAlias string
|
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
|
// Try to get table name from model if it implements TableNameProvider
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
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
|
return b
|
||||||
@@ -95,7 +98,8 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
||||||
b.query = b.query.Table(table)
|
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
|
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 != "" {
|
if prefix == "" && b.tableName != "" {
|
||||||
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
|
// 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 != "" {
|
if prefix == "" && b.tableName != "" {
|
||||||
prefix = b.tableName
|
prefix = b.tableName
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct LEFT JOIN with prefix
|
// 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
|
// GormSelectQuery implements SelectQuery for GORM
|
||||||
type GormSelectQuery struct {
|
type GormSelectQuery struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
tableName string
|
schema string // Separated schema name
|
||||||
|
tableName string // Just the table name, without schema
|
||||||
tableAlias string
|
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
|
// Try to get table name from model if it implements TableNameProvider
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
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
|
return g
|
||||||
@@ -87,7 +90,8 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
||||||
g.db = g.db.Table(table)
|
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
|
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 != "" {
|
if prefix == "" && g.tableName != "" {
|
||||||
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
|
// 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 != "" {
|
if prefix == "" && g.tableName != "" {
|
||||||
prefix = g.tableName
|
prefix = g.tableName
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct LEFT JOIN with prefix
|
// 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")
|
return fmt.Errorf("model cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if modelType.Kind() == reflect.Ptr {
|
originalType := modelType
|
||||||
return fmt.Errorf("model must be a non-pointer struct, got pointer to %s", modelType.Elem().Kind())
|
|
||||||
|
// 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 {
|
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
|
r.models[name] = model
|
||||||
|
|||||||
85
pkg/resolvespec/context.go
Normal file
85
pkg/resolvespec/context.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context keys for request-scoped data
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeySchema contextKey = "schema"
|
||||||
|
contextKeyEntity contextKey = "entity"
|
||||||
|
contextKeyTableName contextKey = "tableName"
|
||||||
|
contextKeyModel contextKey = "model"
|
||||||
|
contextKeyModelPtr contextKey = "modelPtr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithSchema adds schema to context
|
||||||
|
func WithSchema(ctx context.Context, schema string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeySchema, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchema retrieves schema from context
|
||||||
|
func GetSchema(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeySchema); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEntity adds entity to context
|
||||||
|
func WithEntity(ctx context.Context, entity string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyEntity, entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEntity retrieves entity from context
|
||||||
|
func GetEntity(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeyEntity); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTableName adds table name to context
|
||||||
|
func WithTableName(ctx context.Context, tableName string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyTableName, tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTableName retrieves table name from context
|
||||||
|
func GetTableName(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeyTableName); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithModel adds model to context
|
||||||
|
func WithModel(ctx context.Context, model interface{}) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyModel, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModel retrieves model from context
|
||||||
|
func GetModel(ctx context.Context) interface{} {
|
||||||
|
return ctx.Value(contextKeyModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithModelPtr adds model pointer to context
|
||||||
|
func WithModelPtr(ctx context.Context, modelPtr interface{}) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyModelPtr, modelPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelPtr retrieves model pointer from context
|
||||||
|
func GetModelPtr(ctx context.Context) interface{} {
|
||||||
|
return ctx.Value(contextKeyModelPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestData adds all request-scoped data to context at once
|
||||||
|
func WithRequestData(ctx context.Context, schema, entity, tableName string, model, modelPtr interface{}) context.Context {
|
||||||
|
ctx = WithSchema(ctx, schema)
|
||||||
|
ctx = WithEntity(ctx, entity)
|
||||||
|
ctx = WithTableName(ctx, tableName)
|
||||||
|
ctx = WithModel(ctx, model)
|
||||||
|
ctx = WithModelPtr(ctx, modelPtr)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
"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
|
// Handle processes API requests through router-agnostic interface
|
||||||
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
body, err := r.Body()
|
body, err := r.Body()
|
||||||
@@ -50,15 +65,50 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
|
|
||||||
logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity)
|
logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity)
|
||||||
|
|
||||||
|
// Get model and populate context with request-scoped data
|
||||||
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid entity: %v", err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Add request-scoped data to context
|
||||||
|
ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr)
|
||||||
|
|
||||||
switch req.Operation {
|
switch req.Operation {
|
||||||
case "read":
|
case "read":
|
||||||
h.handleRead(ctx, w, schema, entity, id, req.Options)
|
h.handleRead(ctx, w, id, req.Options)
|
||||||
case "create":
|
case "create":
|
||||||
h.handleCreate(ctx, w, schema, entity, req.Data, req.Options)
|
h.handleCreate(ctx, w, req.Data, req.Options)
|
||||||
case "update":
|
case "update":
|
||||||
h.handleUpdate(ctx, w, schema, entity, id, req.ID, req.Data, req.Options)
|
h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options)
|
||||||
case "delete":
|
case "delete":
|
||||||
h.handleDelete(ctx, w, schema, entity, id)
|
h.handleDelete(ctx, w, id)
|
||||||
default:
|
default:
|
||||||
logger.Error("Invalid operation: %s", req.Operation)
|
logger.Error("Invalid operation: %s", req.Operation)
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
|
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
|
||||||
@@ -67,6 +117,13 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
|
|
||||||
// HandleGet processes GET requests for metadata
|
// HandleGet processes GET requests for metadata
|
||||||
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
|
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"]
|
schema := params["schema"]
|
||||||
entity := params["entity"]
|
entity := params["entity"]
|
||||||
|
|
||||||
@@ -83,25 +140,35 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
|||||||
h.sendResponse(w, metadata, nil)
|
h.sendResponse(w, metadata, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schema, entity, id string, options common.RequestOptions) {
|
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options common.RequestOptions) {
|
||||||
logger.Info("Reading records from %s.%s", schema, entity)
|
// Capture panics and return error response
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
h.handlePanic(w, "handleRead", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
schema := GetSchema(ctx)
|
||||||
if err != nil {
|
entity := GetEntity(ctx)
|
||||||
logger.Error("Invalid entity: %v", err)
|
tableName := GetTableName(ctx)
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model is now a non-pointer struct, create a pointer instance for ORM
|
logger.Info("Reading records from %s.%s", schema, entity)
|
||||||
modelType := reflect.TypeOf(model)
|
|
||||||
modelPtr := reflect.New(modelType).Interface()
|
|
||||||
|
|
||||||
query := h.db.NewSelect().Model(modelPtr)
|
// Use Table() with the resolved table name (don't use Model() as it would add the table twice)
|
||||||
|
query := h.db.NewSelect().Table(tableName)
|
||||||
// Get table name
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
query = query.Table(tableName)
|
|
||||||
|
|
||||||
// Apply column selection
|
// Apply column selection
|
||||||
if len(options.Columns) > 0 {
|
if len(options.Columns) > 0 {
|
||||||
@@ -153,7 +220,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
|||||||
var result interface{}
|
var result interface{}
|
||||||
if id != "" {
|
if id != "" {
|
||||||
logger.Debug("Querying single record with ID: %s", id)
|
logger.Debug("Querying single record with ID: %s", id)
|
||||||
// Create a pointer to the struct type for scanning
|
// Create a pointer to the struct type for scanning - use modelType which is already unwrapped
|
||||||
singleResult := reflect.New(modelType).Interface()
|
singleResult := reflect.New(modelType).Interface()
|
||||||
query = query.Where("id = ?", id)
|
query = query.Where("id = ?", id)
|
||||||
if err := query.Scan(ctx, singleResult); err != nil {
|
if err := query.Scan(ctx, singleResult); err != nil {
|
||||||
@@ -164,8 +231,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
|||||||
result = singleResult
|
result = singleResult
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Querying multiple records")
|
logger.Debug("Querying multiple records")
|
||||||
// Create a slice of the struct type (not pointers)
|
// Create a slice of pointers to the model type - use modelType which is already unwrapped
|
||||||
sliceType := reflect.SliceOf(modelType)
|
sliceType := reflect.SliceOf(reflect.PointerTo(modelType))
|
||||||
results := reflect.New(sliceType).Interface()
|
results := reflect.New(sliceType).Interface()
|
||||||
|
|
||||||
if err := query.Scan(ctx, results); err != nil {
|
if err := query.Scan(ctx, results); err != nil {
|
||||||
@@ -195,17 +262,20 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, schema, entity string, data interface{}, options common.RequestOptions) {
|
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)
|
||||||
|
|
||||||
logger.Info("Creating records for %s.%s", schema, entity)
|
logger.Info("Creating records for %s.%s", schema, entity)
|
||||||
|
|
||||||
// Get the model to determine the actual table name
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Model not found, using default table name")
|
|
||||||
model = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
query := h.db.NewInsert().Table(tableName)
|
query := h.db.NewInsert().Table(tableName)
|
||||||
|
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
@@ -275,18 +345,20 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) {
|
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)
|
||||||
|
|
||||||
logger.Info("Updating records for %s.%s", schema, entity)
|
logger.Info("Updating records for %s.%s", schema, entity)
|
||||||
|
|
||||||
// Get the model to determine the actual table name
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Model not found, using default table name")
|
|
||||||
// Fallback to entity name (without schema for SQLite compatibility)
|
|
||||||
model = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
query := h.db.NewUpdate().Table(tableName)
|
query := h.db.NewUpdate().Table(tableName)
|
||||||
|
|
||||||
switch updates := data.(type) {
|
switch updates := data.(type) {
|
||||||
@@ -330,7 +402,18 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
h.sendResponse(w, data, nil)
|
h.sendResponse(w, data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, schema, entity, id string) {
|
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)
|
||||||
|
|
||||||
logger.Info("Deleting records from %s.%s", schema, entity)
|
logger.Info("Deleting records from %s.%s", schema, entity)
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
@@ -339,14 +422,6 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the model to determine the actual table name
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Model not found, using default table name")
|
|
||||||
model = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
query := h.db.NewDelete().Table(tableName).Where("id = ?", id)
|
query := h.db.NewDelete().Table(tableName).Where("id = ?", id)
|
||||||
|
|
||||||
result, err := query.Exec(ctx)
|
result, err := query.Exec(ctx)
|
||||||
@@ -391,19 +466,86 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||||
return provider.TableName()
|
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 {
|
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||||
modelType := reflect.TypeOf(model)
|
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()
|
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{
|
metadata := &common.TableMetadata{
|
||||||
Schema: schema,
|
Schema: schema,
|
||||||
Table: entity,
|
Table: entity,
|
||||||
@@ -547,10 +689,18 @@ type relationshipInfo struct {
|
|||||||
|
|
||||||
func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, preloads []common.PreloadOption) common.SelectQuery {
|
func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, preloads []common.PreloadOption) common.SelectQuery {
|
||||||
modelType := reflect.TypeOf(model)
|
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()
|
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 {
|
for _, preload := range preloads {
|
||||||
logger.Debug("Processing preload for relation: %s", preload.Relation)
|
logger.Debug("Processing preload for relation: %s", preload.Relation)
|
||||||
relInfo := h.getRelationshipInfo(modelType, preload.Relation)
|
relInfo := h.getRelationshipInfo(modelType, preload.Relation)
|
||||||
@@ -574,6 +724,12 @@ func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, pre
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getRelationshipInfo(modelType reflect.Type, relationName string) *relationshipInfo {
|
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++ {
|
for i := 0; i < modelType.NumField(); i++ {
|
||||||
field := modelType.Field(i)
|
field := modelType.Field(i)
|
||||||
jsonTag := field.Tag.Get("json")
|
jsonTag := field.Tag.Get("json")
|
||||||
|
|||||||
85
pkg/restheadspec/context.go
Normal file
85
pkg/restheadspec/context.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package restheadspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context keys for request-scoped data
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeySchema contextKey = "schema"
|
||||||
|
contextKeyEntity contextKey = "entity"
|
||||||
|
contextKeyTableName contextKey = "tableName"
|
||||||
|
contextKeyModel contextKey = "model"
|
||||||
|
contextKeyModelPtr contextKey = "modelPtr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithSchema adds schema to context
|
||||||
|
func WithSchema(ctx context.Context, schema string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeySchema, schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchema retrieves schema from context
|
||||||
|
func GetSchema(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeySchema); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEntity adds entity to context
|
||||||
|
func WithEntity(ctx context.Context, entity string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyEntity, entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEntity retrieves entity from context
|
||||||
|
func GetEntity(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeyEntity); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTableName adds table name to context
|
||||||
|
func WithTableName(ctx context.Context, tableName string) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyTableName, tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTableName retrieves table name from context
|
||||||
|
func GetTableName(ctx context.Context) string {
|
||||||
|
if v := ctx.Value(contextKeyTableName); v != nil {
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithModel adds model to context
|
||||||
|
func WithModel(ctx context.Context, model interface{}) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyModel, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModel retrieves model from context
|
||||||
|
func GetModel(ctx context.Context) interface{} {
|
||||||
|
return ctx.Value(contextKeyModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithModelPtr adds model pointer to context
|
||||||
|
func WithModelPtr(ctx context.Context, modelPtr interface{}) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyModelPtr, modelPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelPtr retrieves model pointer from context
|
||||||
|
func GetModelPtr(ctx context.Context) interface{} {
|
||||||
|
return ctx.Value(contextKeyModelPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestData adds all request-scoped data to context at once
|
||||||
|
func WithRequestData(ctx context.Context, schema, entity, tableName string, model, modelPtr interface{}) context.Context {
|
||||||
|
ctx = WithSchema(ctx, schema)
|
||||||
|
ctx = WithEntity(ctx, entity)
|
||||||
|
ctx = WithTableName(ctx, tableName)
|
||||||
|
ctx = WithModel(ctx, model)
|
||||||
|
ctx = WithModelPtr(ctx, modelPtr)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
"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
|
// Handle processes API requests through router-agnostic interface
|
||||||
// Options are read from HTTP headers instead of request body
|
// Options are read from HTTP headers instead of request body
|
||||||
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
schema := params["schema"]
|
schema := params["schema"]
|
||||||
@@ -44,14 +59,48 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
|
|
||||||
logger.Info("Handling %s request for %s.%s", method, schema, entity)
|
logger.Info("Handling %s request for %s.%s", method, schema, entity)
|
||||||
|
|
||||||
|
// Get model and populate context with request-scoped data
|
||||||
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid entity: %v", err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Add request-scoped data to context
|
||||||
|
ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr)
|
||||||
|
|
||||||
switch method {
|
switch method {
|
||||||
case "GET":
|
case "GET":
|
||||||
if id != "" {
|
if id != "" {
|
||||||
// GET with ID - read single record
|
// GET with ID - read single record
|
||||||
h.handleRead(ctx, w, schema, entity, id, options)
|
h.handleRead(ctx, w, id, options)
|
||||||
} else {
|
} else {
|
||||||
// GET without ID - read multiple records
|
// GET without ID - read multiple records
|
||||||
h.handleRead(ctx, w, schema, entity, "", options)
|
h.handleRead(ctx, w, "", options)
|
||||||
}
|
}
|
||||||
case "POST":
|
case "POST":
|
||||||
// Create operation
|
// Create operation
|
||||||
@@ -67,7 +116,7 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
|
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.handleCreate(ctx, w, schema, entity, data, options)
|
h.handleCreate(ctx, w, data, options)
|
||||||
case "PUT", "PATCH":
|
case "PUT", "PATCH":
|
||||||
// Update operation
|
// Update operation
|
||||||
body, err := r.Body()
|
body, err := r.Body()
|
||||||
@@ -82,9 +131,9 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
|
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.handleUpdate(ctx, w, schema, entity, id, nil, data, options)
|
h.handleUpdate(ctx, w, id, nil, data, options)
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
h.handleDelete(ctx, w, schema, entity, id)
|
h.handleDelete(ctx, w, id)
|
||||||
default:
|
default:
|
||||||
logger.Error("Invalid HTTP method: %s", method)
|
logger.Error("Invalid HTTP method: %s", method)
|
||||||
h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil)
|
h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil)
|
||||||
@@ -93,6 +142,13 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
|
|
||||||
// HandleGet processes GET requests for metadata
|
// HandleGet processes GET requests for metadata
|
||||||
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
|
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"]
|
schema := params["schema"]
|
||||||
entity := params["entity"]
|
entity := params["entity"]
|
||||||
|
|
||||||
@@ -111,21 +167,38 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
|||||||
|
|
||||||
// parseOptionsFromHeaders is now implemented in headers.go
|
// parseOptionsFromHeaders is now implemented in headers.go
|
||||||
|
|
||||||
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schema, entity, id string, options ExtendedRequestOptions) {
|
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options ExtendedRequestOptions) {
|
||||||
logger.Info("Reading records from %s.%s", schema, entity)
|
// Capture panics and return error response
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
h.handlePanic(w, "handleRead", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
schema := GetSchema(ctx)
|
||||||
if err != nil {
|
entity := GetEntity(ctx)
|
||||||
logger.Error("Invalid entity: %v", err)
|
tableName := GetTableName(ctx)
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
query := h.db.NewSelect().Model(model)
|
// Create a pointer to a slice of pointers to the model type for query results
|
||||||
|
modelPtr := reflect.New(reflect.SliceOf(reflect.PointerTo(modelType))).Interface()
|
||||||
|
|
||||||
// Get table name
|
logger.Info("Reading records from %s.%s", schema, entity)
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
query = query.Table(tableName)
|
// Use Table() with the resolved table name (don't use Model() as it would add the table twice)
|
||||||
|
query := h.db.NewSelect().Table(tableName)
|
||||||
|
|
||||||
// Apply column selection
|
// Apply column selection
|
||||||
if len(options.Columns) > 0 {
|
if len(options.Columns) > 0 {
|
||||||
@@ -214,9 +287,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
|||||||
query = query.Offset(*options.Offset)
|
query = query.Offset(*options.Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query - modelPtr was already created earlier
|
||||||
resultSlice := reflect.New(reflect.SliceOf(reflect.TypeOf(model))).Interface()
|
if err := query.Scan(ctx, modelPtr); err != nil {
|
||||||
if err := query.Scan(ctx, resultSlice); err != nil {
|
|
||||||
logger.Error("Error executing query: %v", err)
|
logger.Error("Error executing query: %v", err)
|
||||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||||
return
|
return
|
||||||
@@ -238,21 +310,24 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
|||||||
Offset: offset,
|
Offset: offset,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.sendFormattedResponse(w, resultSlice, metadata, options)
|
h.sendFormattedResponse(w, modelPtr, metadata, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, schema, entity string, data interface{}, options ExtendedRequestOptions) {
|
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)
|
||||||
|
model := GetModel(ctx)
|
||||||
|
|
||||||
logger.Info("Creating record in %s.%s", schema, entity)
|
logger.Info("Creating record in %s.%s", schema, entity)
|
||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Invalid entity: %v", err)
|
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
|
|
||||||
// Handle batch creation
|
// Handle batch creation
|
||||||
dataValue := reflect.ValueOf(data)
|
dataValue := reflect.ValueOf(data)
|
||||||
if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array {
|
if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array {
|
||||||
@@ -263,8 +338,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
for i := 0; i < dataValue.Len(); i++ {
|
for i := 0; i < dataValue.Len(); i++ {
|
||||||
item := dataValue.Index(i).Interface()
|
item := dataValue.Index(i).Interface()
|
||||||
|
|
||||||
// Convert item to model type
|
// Convert item to model type - create a pointer to the model
|
||||||
modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
||||||
jsonData, err := json.Marshal(item)
|
jsonData, err := json.Marshal(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal item: %w", err)
|
return fmt.Errorf("failed to marshal item: %w", err)
|
||||||
@@ -291,8 +366,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single record creation
|
// Single record creation - create a pointer to the model
|
||||||
modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error marshaling data: %v", err)
|
logger.Error("Error marshaling data: %v", err)
|
||||||
@@ -315,18 +390,20 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
h.sendResponse(w, modelValue, nil)
|
h.sendResponse(w, modelValue, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, schema, entity, id string, idPtr *int64, data interface{}, options ExtendedRequestOptions) {
|
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)
|
||||||
|
|
||||||
logger.Info("Updating record in %s.%s", schema, entity)
|
logger.Info("Updating record in %s.%s", schema, entity)
|
||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Invalid entity: %v", err)
|
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
|
|
||||||
// Convert data to map
|
// Convert data to map
|
||||||
dataMap, ok := data.(map[string]interface{})
|
dataMap, ok := data.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -367,18 +444,20 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch
|
|||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, schema, entity, id string) {
|
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)
|
||||||
|
|
||||||
logger.Info("Deleting record from %s.%s", schema, entity)
|
logger.Info("Deleting record from %s.%s", schema, entity)
|
||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Invalid entity: %v", err)
|
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tableName := h.getTableName(schema, entity, model)
|
|
||||||
|
|
||||||
query := h.db.NewDelete().Table(tableName)
|
query := h.db.NewDelete().Table(tableName)
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
@@ -452,28 +531,85 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
// Check if model implements TableNameProvider
|
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||||
tableName := provider.TableName()
|
return fullTableName[:idx], fullTableName[idx+1:]
|
||||||
if tableName != "" {
|
}
|
||||||
return tableName
|
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
|
// No TableNameProvider, so check for schema and use entity as table name
|
||||||
if schema != "" {
|
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||||
return fmt.Sprintf("%s.%s", schema, entity)
|
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 {
|
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||||
modelType := reflect.TypeOf(model)
|
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()
|
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)
|
tableName := h.getTableName(schema, entity, model)
|
||||||
|
|
||||||
metadata := &common.TableMetadata{
|
metadata := &common.TableMetadata{
|
||||||
@@ -559,7 +695,7 @@ func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{
|
|||||||
if options.CleanJSON {
|
if options.CleanJSON {
|
||||||
data = h.cleanJSON(data)
|
data = h.cleanJSON(data)
|
||||||
}
|
}
|
||||||
|
w.SetHeader("Content-Type", "application/json")
|
||||||
// Format response based on response format option
|
// Format response based on response format option
|
||||||
switch options.ResponseFormat {
|
switch options.ResponseFormat {
|
||||||
case "simple":
|
case "simple":
|
||||||
|
|||||||
@@ -19,21 +19,21 @@ type ExtendedRequestOptions struct {
|
|||||||
CleanJSON bool
|
CleanJSON bool
|
||||||
|
|
||||||
// Advanced filtering
|
// Advanced filtering
|
||||||
SearchColumns []string
|
SearchColumns []string
|
||||||
CustomSQLWhere string
|
CustomSQLWhere string
|
||||||
CustomSQLOr string
|
CustomSQLOr string
|
||||||
|
|
||||||
// Joins
|
// Joins
|
||||||
Expand []ExpandOption
|
Expand []ExpandOption
|
||||||
|
|
||||||
// Advanced features
|
// Advanced features
|
||||||
AdvancedSQL map[string]string // Column -> SQL expression
|
AdvancedSQL map[string]string // Column -> SQL expression
|
||||||
ComputedQL map[string]string // Column -> CQL expression
|
ComputedQL map[string]string // Column -> CQL expression
|
||||||
Distinct bool
|
Distinct bool
|
||||||
SkipCount bool
|
SkipCount bool
|
||||||
SkipCache bool
|
SkipCache bool
|
||||||
FetchRowNumber *string
|
FetchRowNumber *string
|
||||||
PKRow *string
|
PKRow *string
|
||||||
|
|
||||||
// Response format
|
// Response format
|
||||||
ResponseFormat string // "simple", "detail", "syncfusion"
|
ResponseFormat string // "simple", "detail", "syncfusion"
|
||||||
@@ -42,16 +42,16 @@ type ExtendedRequestOptions struct {
|
|||||||
AtomicTransaction bool
|
AtomicTransaction bool
|
||||||
|
|
||||||
// Cursor pagination
|
// Cursor pagination
|
||||||
CursorForward string
|
CursorForward string
|
||||||
CursorBackward string
|
CursorBackward string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpandOption represents a relation expansion configuration
|
// ExpandOption represents a relation expansion configuration
|
||||||
type ExpandOption struct {
|
type ExpandOption struct {
|
||||||
Relation string
|
Relation string
|
||||||
Columns []string
|
Columns []string
|
||||||
Where string
|
Where string
|
||||||
Sort string
|
Sort string
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeHeaderValue decodes base64 encoded header values
|
// decodeHeaderValue decodes base64 encoded header values
|
||||||
@@ -85,12 +85,13 @@ func (h *Handler) parseOptionsFromHeaders(r common.Request) ExtendedRequestOptio
|
|||||||
options := ExtendedRequestOptions{
|
options := ExtendedRequestOptions{
|
||||||
RequestOptions: common.RequestOptions{
|
RequestOptions: common.RequestOptions{
|
||||||
Filters: make([]common.FilterOption, 0),
|
Filters: make([]common.FilterOption, 0),
|
||||||
Sort: make([]common.SortOption, 0),
|
Sort: make([]common.SortOption, 0),
|
||||||
Preload: make([]common.PreloadOption, 0),
|
Preload: make([]common.PreloadOption, 0),
|
||||||
},
|
},
|
||||||
AdvancedSQL: make(map[string]string),
|
AdvancedSQL: make(map[string]string),
|
||||||
ComputedQL: make(map[string]string),
|
ComputedQL: make(map[string]string),
|
||||||
Expand: make([]ExpandOption, 0),
|
Expand: make([]ExpandOption, 0),
|
||||||
|
ResponseFormat: "simple", // Default response format
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all headers
|
// Get all headers
|
||||||
@@ -212,9 +213,9 @@ func (h *Handler) parseNotSelectFields(options *ExtendedRequestOptions, value st
|
|||||||
func (h *Handler) parseFieldFilter(options *ExtendedRequestOptions, headerKey, value string) {
|
func (h *Handler) parseFieldFilter(options *ExtendedRequestOptions, headerKey, value string) {
|
||||||
colName := strings.TrimPrefix(headerKey, "x-fieldfilter-")
|
colName := strings.TrimPrefix(headerKey, "x-fieldfilter-")
|
||||||
options.Filters = append(options.Filters, common.FilterOption{
|
options.Filters = append(options.Filters, common.FilterOption{
|
||||||
Column: colName,
|
Column: colName,
|
||||||
Operator: "eq",
|
Operator: "eq",
|
||||||
Value: value,
|
Value: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,9 +224,9 @@ func (h *Handler) parseSearchFilter(options *ExtendedRequestOptions, headerKey,
|
|||||||
colName := strings.TrimPrefix(headerKey, "x-searchfilter-")
|
colName := strings.TrimPrefix(headerKey, "x-searchfilter-")
|
||||||
// Use ILIKE for fuzzy search
|
// Use ILIKE for fuzzy search
|
||||||
options.Filters = append(options.Filters, common.FilterOption{
|
options.Filters = append(options.Filters, common.FilterOption{
|
||||||
Column: colName,
|
Column: colName,
|
||||||
Operator: "ilike",
|
Operator: "ilike",
|
||||||
Value: "%" + value + "%",
|
Value: "%" + value + "%",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +408,7 @@ func (h *Handler) parseSorting(options *ExtendedRequestOptions, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options.Sort = append(options.Sort, common.SortOption{
|
options.Sort = append(options.Sort, common.SortOption{
|
||||||
Column: colName,
|
Column: colName,
|
||||||
Direction: direction,
|
Direction: direction,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,55 @@
|
|||||||
|
// Package restheadspec provides the Rest Header Spec API framework.
|
||||||
|
//
|
||||||
|
// Rest Header Spec (restheadspec) is a RESTful API framework that reads query options,
|
||||||
|
// filters, sorting, pagination, and other parameters from HTTP headers instead of
|
||||||
|
// request bodies or query parameters. This approach provides a clean separation between
|
||||||
|
// data and metadata in API requests.
|
||||||
|
//
|
||||||
|
// # Key Features
|
||||||
|
//
|
||||||
|
// - Header-based API configuration: All query options are passed via HTTP headers
|
||||||
|
// - Database-agnostic: Works with both GORM and Bun ORM through adapters
|
||||||
|
// - Router-agnostic: Supports multiple HTTP routers (Mux, BunRouter, etc.)
|
||||||
|
// - Advanced filtering: Supports complex filter operations (eq, gt, lt, like, between, etc.)
|
||||||
|
// - Pagination and sorting: Built-in support for limit, offset, and multi-column sorting
|
||||||
|
// - Preloading and expansion: Support for eager loading relationships
|
||||||
|
// - Multiple response formats: Default, simple, and Syncfusion formats
|
||||||
|
//
|
||||||
|
// # HTTP Headers
|
||||||
|
//
|
||||||
|
// The following headers are supported for configuring API requests:
|
||||||
|
//
|
||||||
|
// - X-Filters: JSON array of filter conditions
|
||||||
|
// - X-Columns: Comma-separated list of columns to select
|
||||||
|
// - X-Sort: JSON array of sort specifications
|
||||||
|
// - X-Limit: Maximum number of records to return
|
||||||
|
// - X-Offset: Number of records to skip
|
||||||
|
// - X-Preload: Comma-separated list of relations to preload
|
||||||
|
// - X-Expand: Comma-separated list of relations to expand (LEFT JOIN)
|
||||||
|
// - X-Distinct: Boolean to enable DISTINCT queries
|
||||||
|
// - X-Skip-Count: Boolean to skip total count query
|
||||||
|
// - X-Response-Format: Response format (detail, simple, syncfusion)
|
||||||
|
// - X-Clean-JSON: Boolean to remove null/empty fields
|
||||||
|
// - X-Custom-SQL-Where: Custom SQL WHERE clause (AND)
|
||||||
|
// - X-Custom-SQL-Or: Custom SQL WHERE clause (OR)
|
||||||
|
//
|
||||||
|
// # Usage Example
|
||||||
|
//
|
||||||
|
// // Create a handler with GORM
|
||||||
|
// handler := restheadspec.NewHandlerWithGORM(db)
|
||||||
|
//
|
||||||
|
// // Register models
|
||||||
|
// handler.Registry.RegisterModel("users", User{})
|
||||||
|
//
|
||||||
|
// // Setup routes with Mux
|
||||||
|
// muxRouter := mux.NewRouter()
|
||||||
|
// restheadspec.SetupMuxRoutes(muxRouter, handler)
|
||||||
|
//
|
||||||
|
// // Make a request with headers
|
||||||
|
// // GET /public/users
|
||||||
|
// // X-Filters: [{"column":"age","operator":"gt","value":18}]
|
||||||
|
// // X-Sort: [{"column":"name","direction":"asc"}]
|
||||||
|
// // X-Limit: 10
|
||||||
package restheadspec
|
package restheadspec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user