mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-30 00:04:25 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e5754a47 | ||
|
|
c88bff1883 | ||
|
|
d122c7af42 |
@@ -15,12 +15,12 @@ if [[ $make_release =~ ^[Yy]$ ]]; then
|
||||
fi
|
||||
|
||||
# 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
|
||||
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
|
||||
echo "No release version created."
|
||||
fi
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
||||
"github.com/uptrace/bun"
|
||||
@@ -76,16 +77,25 @@ func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Databa
|
||||
|
||||
// BunSelectQuery implements SelectQuery for Bun
|
||||
type BunSelectQuery struct {
|
||||
query *bun.SelectQuery
|
||||
query *bun.SelectQuery
|
||||
tableName string
|
||||
tableAlias string
|
||||
}
|
||||
|
||||
func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
b.query = b.query.Model(model)
|
||||
|
||||
// Try to get table name from model if it implements TableNameProvider
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
b.tableName = provider.TableName()
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
||||
b.query = b.query.Table(table)
|
||||
b.tableName = table
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -105,12 +115,81 @@ func (b *BunSelectQuery) WhereOr(query string, args ...interface{}) common.Selec
|
||||
}
|
||||
|
||||
func (b *BunSelectQuery) Join(query string, args ...interface{}) common.SelectQuery {
|
||||
b.query = b.query.Join(query, args...)
|
||||
// Extract optional prefix from args
|
||||
// If the last arg is a string that looks like a table prefix, use it
|
||||
var prefix string
|
||||
sqlArgs := args
|
||||
|
||||
if len(args) > 0 {
|
||||
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
|
||||
// Likely a prefix, not a SQL parameter
|
||||
prefix = lastArg
|
||||
sqlArgs = args[:len(args)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
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
|
||||
// Bun expects: "JOIN table AS alias ON condition"
|
||||
joinClause := query
|
||||
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
|
||||
// If query doesn't already have AS, check if it's a simple table name
|
||||
parts := strings.Fields(query)
|
||||
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
|
||||
// Simple table name, add prefix: "table AS prefix"
|
||||
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
|
||||
if len(parts) > 1 {
|
||||
// Has ON clause: "table ON ..." becomes "table AS prefix ON ..."
|
||||
joinClause += " " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.query = b.query.Join(joinClause, sqlArgs...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *BunSelectQuery) LeftJoin(query string, args ...interface{}) common.SelectQuery {
|
||||
b.query = b.query.Join("LEFT JOIN " + query, args...)
|
||||
// Extract optional prefix from args
|
||||
var prefix string
|
||||
sqlArgs := args
|
||||
|
||||
if len(args) > 0 {
|
||||
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
|
||||
prefix = lastArg
|
||||
sqlArgs = args[:len(args)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
if prefix == "" && b.tableName != "" {
|
||||
prefix = b.tableName
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Construct LEFT JOIN with prefix
|
||||
joinClause := query
|
||||
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
|
||||
parts := strings.Fields(query)
|
||||
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "LEFT") && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
|
||||
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
|
||||
if len(parts) > 1 {
|
||||
joinClause += " " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.query = b.query.Join("LEFT JOIN " + joinClause, sqlArgs...)
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/common"
|
||||
"gorm.io/gorm"
|
||||
@@ -67,16 +69,25 @@ func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Datab
|
||||
|
||||
// GormSelectQuery implements SelectQuery for GORM
|
||||
type GormSelectQuery struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
tableName string
|
||||
tableAlias string
|
||||
}
|
||||
|
||||
func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
||||
g.db = g.db.Model(model)
|
||||
|
||||
// Try to get table name from model if it implements TableNameProvider
|
||||
if provider, ok := model.(common.TableNameProvider); ok {
|
||||
g.tableName = provider.TableName()
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
||||
g.db = g.db.Table(table)
|
||||
g.tableName = table
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -96,12 +107,81 @@ func (g *GormSelectQuery) WhereOr(query string, args ...interface{}) common.Sele
|
||||
}
|
||||
|
||||
func (g *GormSelectQuery) Join(query string, args ...interface{}) common.SelectQuery {
|
||||
g.db = g.db.Joins(query, args...)
|
||||
// Extract optional prefix from args
|
||||
// If the last arg is a string that looks like a table prefix, use it
|
||||
var prefix string
|
||||
sqlArgs := args
|
||||
|
||||
if len(args) > 0 {
|
||||
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
|
||||
// Likely a prefix, not a SQL parameter
|
||||
prefix = lastArg
|
||||
sqlArgs = args[:len(args)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
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
|
||||
// GORM expects: "JOIN table AS alias ON condition"
|
||||
joinClause := query
|
||||
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
|
||||
// If query doesn't already have AS, check if it's a simple table name
|
||||
parts := strings.Fields(query)
|
||||
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
|
||||
// Simple table name, add prefix: "table AS prefix"
|
||||
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
|
||||
if len(parts) > 1 {
|
||||
// Has ON clause: "table ON ..." becomes "table AS prefix ON ..."
|
||||
joinClause += " " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.db = g.db.Joins(joinClause, sqlArgs...)
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GormSelectQuery) LeftJoin(query string, args ...interface{}) common.SelectQuery {
|
||||
g.db = g.db.Joins("LEFT JOIN "+query, args...)
|
||||
// Extract optional prefix from args
|
||||
var prefix string
|
||||
sqlArgs := args
|
||||
|
||||
if len(args) > 0 {
|
||||
if lastArg, ok := args[len(args)-1].(string); ok && len(lastArg) < 50 && !strings.Contains(lastArg, " ") {
|
||||
prefix = lastArg
|
||||
sqlArgs = args[:len(args)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// If no prefix provided, use the table name as prefix
|
||||
if prefix == "" && g.tableName != "" {
|
||||
prefix = g.tableName
|
||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
||||
prefix = prefix[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Construct LEFT JOIN with prefix
|
||||
joinClause := query
|
||||
if prefix != "" && !strings.Contains(strings.ToUpper(query), " AS ") {
|
||||
parts := strings.Fields(query)
|
||||
if len(parts) > 0 && !strings.HasPrefix(strings.ToUpper(parts[0]), "LEFT") && !strings.HasPrefix(strings.ToUpper(parts[0]), "JOIN") {
|
||||
joinClause = fmt.Sprintf("%s AS %s", parts[0], prefix)
|
||||
if len(parts) > 1 {
|
||||
joinClause += " " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.db = g.db.Joins("LEFT JOIN "+joinClause, sqlArgs...)
|
||||
return g
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package modelregistry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -26,11 +27,25 @@ func NewModelRegistry() *DefaultModelRegistry {
|
||||
func (r *DefaultModelRegistry) RegisterModel(name string, model interface{}) error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
|
||||
if _, exists := r.models[name]; exists {
|
||||
return fmt.Errorf("model %s already registered", name)
|
||||
}
|
||||
|
||||
|
||||
// Validate that model is a non-pointer struct
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType == nil {
|
||||
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())
|
||||
}
|
||||
|
||||
if modelType.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("model must be a struct, got %s", modelType.Kind())
|
||||
}
|
||||
|
||||
r.models[name] = model
|
||||
return nil
|
||||
}
|
||||
|
||||
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"
|
||||
"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()
|
||||
@@ -50,15 +65,30 @@ 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
case "read":
|
||||
h.handleRead(ctx, w, schema, entity, id, req.Options)
|
||||
h.handleRead(ctx, w, id, req.Options)
|
||||
case "create":
|
||||
h.handleCreate(ctx, w, schema, entity, req.Data, req.Options)
|
||||
h.handleCreate(ctx, w, req.Data, req.Options)
|
||||
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":
|
||||
h.handleDelete(ctx, w, schema, entity, id)
|
||||
h.handleDelete(ctx, w, id)
|
||||
default:
|
||||
logger.Error("Invalid operation: %s", req.Operation)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
|
||||
@@ -67,6 +97,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"]
|
||||
|
||||
@@ -83,20 +120,23 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
||||
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) {
|
||||
// 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)
|
||||
|
||||
logger.Info("Reading records 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
|
||||
}
|
||||
|
||||
query := h.db.NewSelect().Model(model)
|
||||
|
||||
// Get table name
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
query := h.db.NewSelect().Model(modelPtr)
|
||||
query = query.Table(tableName)
|
||||
|
||||
// Apply column selection
|
||||
@@ -149,7 +189,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
||||
var result interface{}
|
||||
if id != "" {
|
||||
logger.Debug("Querying single record with ID: %s", id)
|
||||
singleResult := model
|
||||
// Create a pointer to the struct type for scanning
|
||||
singleResult := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
query = query.Where("id = ?", id)
|
||||
if err := query.Scan(ctx, singleResult); err != nil {
|
||||
logger.Error("Error querying record: %v", err)
|
||||
@@ -159,7 +200,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
||||
result = singleResult
|
||||
} else {
|
||||
logger.Debug("Querying multiple records")
|
||||
sliceType := reflect.SliceOf(reflect.TypeOf(model))
|
||||
// Create a slice of pointers to the model type
|
||||
sliceType := reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(model)))
|
||||
results := reflect.New(sliceType).Interface()
|
||||
|
||||
if err := query.Scan(ctx, results); err != nil {
|
||||
@@ -189,17 +231,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)
|
||||
|
||||
// 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)
|
||||
|
||||
switch v := data.(type) {
|
||||
@@ -269,18 +314,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)
|
||||
|
||||
// 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)
|
||||
|
||||
switch updates := data.(type) {
|
||||
@@ -324,7 +371,18 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch
|
||||
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)
|
||||
|
||||
if id == "" {
|
||||
@@ -333,14 +391,6 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, sch
|
||||
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)
|
||||
|
||||
result, err := query.Exec(ctx)
|
||||
|
||||
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"
|
||||
"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"]
|
||||
@@ -44,14 +59,28 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
case "GET":
|
||||
if id != "" {
|
||||
// GET with ID - read single record
|
||||
h.handleRead(ctx, w, schema, entity, id, options)
|
||||
h.handleRead(ctx, w, id, options)
|
||||
} else {
|
||||
// GET without ID - read multiple records
|
||||
h.handleRead(ctx, w, schema, entity, "", options)
|
||||
h.handleRead(ctx, w, "", options)
|
||||
}
|
||||
case "POST":
|
||||
// Create operation
|
||||
@@ -67,7 +96,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)
|
||||
return
|
||||
}
|
||||
h.handleCreate(ctx, w, schema, entity, data, options)
|
||||
h.handleCreate(ctx, w, data, options)
|
||||
case "PUT", "PATCH":
|
||||
// Update operation
|
||||
body, err := r.Body()
|
||||
@@ -82,9 +111,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)
|
||||
return
|
||||
}
|
||||
h.handleUpdate(ctx, w, schema, entity, id, nil, data, options)
|
||||
h.handleUpdate(ctx, w, id, nil, data, options)
|
||||
case "DELETE":
|
||||
h.handleDelete(ctx, w, schema, entity, id)
|
||||
h.handleDelete(ctx, w, id)
|
||||
default:
|
||||
logger.Error("Invalid HTTP method: %s", method)
|
||||
h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil)
|
||||
@@ -93,6 +122,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"]
|
||||
|
||||
@@ -111,20 +147,22 @@ 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, schema, entity, id string, options ExtendedRequestOptions) {
|
||||
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)
|
||||
|
||||
logger.Info("Reading records 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
|
||||
}
|
||||
|
||||
query := h.db.NewSelect().Model(model)
|
||||
|
||||
// Get table name
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
query := h.db.NewSelect().Model(modelPtr)
|
||||
query = query.Table(tableName)
|
||||
|
||||
// Apply column selection
|
||||
@@ -214,8 +252,9 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
||||
query = query.Offset(*options.Offset)
|
||||
}
|
||||
|
||||
// Execute query
|
||||
resultSlice := reflect.New(reflect.SliceOf(reflect.TypeOf(model))).Interface()
|
||||
// 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 {
|
||||
logger.Error("Error executing query: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||
@@ -241,18 +280,21 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem
|
||||
h.sendFormattedResponse(w, resultSlice, 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)
|
||||
|
||||
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
|
||||
dataValue := reflect.ValueOf(data)
|
||||
if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array {
|
||||
@@ -263,8 +305,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
||||
for i := 0; i < dataValue.Len(); i++ {
|
||||
item := dataValue.Index(i).Interface()
|
||||
|
||||
// Convert item to model type
|
||||
modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
// Convert item to model type - create a pointer to the model
|
||||
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
jsonData, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal item: %w", err)
|
||||
@@ -291,8 +333,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
||||
return
|
||||
}
|
||||
|
||||
// Single record creation
|
||||
modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
||||
// Single record creation - create a pointer to the model
|
||||
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
logger.Error("Error marshaling data: %v", err)
|
||||
@@ -315,18 +357,20 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch
|
||||
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)
|
||||
|
||||
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
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -367,18 +411,20 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch
|
||||
}, 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)
|
||||
|
||||
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)
|
||||
|
||||
if id == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
|
||||
@@ -140,22 +140,22 @@ func (Comment) TableName() string {
|
||||
|
||||
// RegisterTestModels registers all test models with the provided registry
|
||||
func RegisterTestModels(registry *modelregistry.DefaultModelRegistry) {
|
||||
registry.RegisterModel("departments", &Department{})
|
||||
registry.RegisterModel("employees", &Employee{})
|
||||
registry.RegisterModel("projects", &Project{})
|
||||
registry.RegisterModel("project_tasks", &ProjectTask{})
|
||||
registry.RegisterModel("documents", &Document{})
|
||||
registry.RegisterModel("comments", &Comment{})
|
||||
registry.RegisterModel("departments", Department{})
|
||||
registry.RegisterModel("employees", Employee{})
|
||||
registry.RegisterModel("projects", Project{})
|
||||
registry.RegisterModel("project_tasks", ProjectTask{})
|
||||
registry.RegisterModel("documents", Document{})
|
||||
registry.RegisterModel("comments", Comment{})
|
||||
}
|
||||
|
||||
// GetTestModels returns a list of all test model instances
|
||||
func GetTestModels() []interface{} {
|
||||
return []interface{}{
|
||||
&Department{},
|
||||
&Employee{},
|
||||
&Project{},
|
||||
&ProjectTask{},
|
||||
&Document{},
|
||||
&Comment{},
|
||||
Department{},
|
||||
Employee{},
|
||||
Project{},
|
||||
ProjectTask{},
|
||||
Document{},
|
||||
Comment{},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user