Massive refactor and introduction of restheadspec

This commit is contained in:
Hein
2025-11-06 16:15:35 +02:00
parent 399cea9335
commit 8e06736701
22 changed files with 2472 additions and 1046 deletions

View File

@@ -1,91 +0,0 @@
package resolvespec
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
"gorm.io/gorm"
)
type HandlerFunc func(http.ResponseWriter, *http.Request)
type LegacyAPIHandler struct {
db *gorm.DB
}
// NewLegacyAPIHandler creates a new legacy API handler instance
func NewLegacyAPIHandler(db *gorm.DB) *LegacyAPIHandler {
return &LegacyAPIHandler{
db: db,
}
}
// Main handler method
func (h *LegacyAPIHandler) Handle(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req RequestBody
if r.Body == nil {
logger.Error("No body to decode")
h.sendError(w, http.StatusBadRequest, "invalid_request", "No body to decode", nil)
return
} else {
defer r.Body.Close()
}
if bodyContents, err := io.ReadAll(r.Body); err != nil {
logger.Error("Failed to decode read body: %v", err)
h.sendError(w, http.StatusBadRequest, "read_request", "Invalid request body", err)
return
} else {
if err := json.Unmarshal(bodyContents, &req); err != nil {
logger.Error("Failed to decode request body: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
return
}
}
schema := params["schema"]
entity := params["entity"]
id := params["id"]
logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity)
switch req.Operation {
case "read":
h.handleRead(w, r, schema, entity, id, req.Options)
case "create":
h.handleCreate(w, r, schema, entity, req.Data, req.Options)
case "update":
h.handleUpdate(w, r, schema, entity, id, req.ID, req.Data, req.Options)
case "delete":
h.handleDelete(w, r, schema, entity, id)
default:
logger.Error("Invalid operation: %s", req.Operation)
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
}
}
func (h *LegacyAPIHandler) sendResponse(w http.ResponseWriter, data interface{}, metadata *Metadata) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: true,
Data: data,
Metadata: metadata,
})
}
func (h *LegacyAPIHandler) sendError(w http.ResponseWriter, status int, code, message string, details interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Success: false,
Error: &APIError{
Code: code,
Message: message,
Details: details,
Detail: fmt.Sprintf("%v", details),
},
})
}

View File

@@ -1,334 +0,0 @@
package resolvespec
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
// BunAdapter adapts Bun to work with our Database interface
// This demonstrates how the abstraction works with different ORMs
type BunAdapter struct {
db *bun.DB
}
// NewBunAdapter creates a new Bun adapter
func NewBunAdapter(db *bun.DB) *BunAdapter {
return &BunAdapter{db: db}
}
func (b *BunAdapter) NewSelect() SelectQuery {
return &BunSelectQuery{query: b.db.NewSelect()}
}
func (b *BunAdapter) NewInsert() InsertQuery {
return &BunInsertQuery{query: b.db.NewInsert()}
}
func (b *BunAdapter) NewUpdate() UpdateQuery {
return &BunUpdateQuery{query: b.db.NewUpdate()}
}
func (b *BunAdapter) NewDelete() DeleteQuery {
return &BunDeleteQuery{query: b.db.NewDelete()}
}
func (b *BunAdapter) Exec(ctx context.Context, query string, args ...interface{}) (Result, error) {
result, err := b.db.ExecContext(ctx, query, args...)
return &BunResult{result: result}, err
}
func (b *BunAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return b.db.NewRaw(query, args...).Scan(ctx, dest)
}
func (b *BunAdapter) BeginTx(ctx context.Context) (Database, error) {
tx, err := b.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return nil, err
}
// For Bun, we'll return a special wrapper that holds the transaction
return &BunTxAdapter{tx: tx}, nil
}
func (b *BunAdapter) CommitTx(ctx context.Context) error {
// For Bun, we need to handle this differently
// This is a simplified implementation
return nil
}
func (b *BunAdapter) RollbackTx(ctx context.Context) error {
// For Bun, we need to handle this differently
// This is a simplified implementation
return nil
}
func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(Database) error) error {
return b.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
// Create adapter with transaction
adapter := &BunTxAdapter{tx: tx}
return fn(adapter)
})
}
// BunSelectQuery implements SelectQuery for Bun
type BunSelectQuery struct {
query *bun.SelectQuery
}
func (b *BunSelectQuery) Model(model interface{}) SelectQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunSelectQuery) Table(table string) SelectQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunSelectQuery) Column(columns ...string) SelectQuery {
b.query = b.query.Column(columns...)
return b
}
func (b *BunSelectQuery) Where(query string, args ...interface{}) SelectQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunSelectQuery) WhereOr(query string, args ...interface{}) SelectQuery {
b.query = b.query.WhereOr(query, args...)
return b
}
func (b *BunSelectQuery) Join(query string, args ...interface{}) SelectQuery {
b.query = b.query.Join(query, args...)
return b
}
func (b *BunSelectQuery) LeftJoin(query string, args ...interface{}) SelectQuery {
b.query = b.query.Join("LEFT JOIN " + query, args...)
return b
}
func (b *BunSelectQuery) Order(order string) SelectQuery {
b.query = b.query.Order(order)
return b
}
func (b *BunSelectQuery) Limit(n int) SelectQuery {
b.query = b.query.Limit(n)
return b
}
func (b *BunSelectQuery) Offset(n int) SelectQuery {
b.query = b.query.Offset(n)
return b
}
func (b *BunSelectQuery) Group(group string) SelectQuery {
b.query = b.query.Group(group)
return b
}
func (b *BunSelectQuery) Having(having string, args ...interface{}) SelectQuery {
b.query = b.query.Having(having, args...)
return b
}
func (b *BunSelectQuery) Scan(ctx context.Context, dest interface{}) error {
return b.query.Scan(ctx, dest)
}
func (b *BunSelectQuery) Count(ctx context.Context) (int, error) {
count, err := b.query.Count(ctx)
return count, err
}
func (b *BunSelectQuery) Exists(ctx context.Context) (bool, error) {
return b.query.Exists(ctx)
}
// BunInsertQuery implements InsertQuery for Bun
type BunInsertQuery struct {
query *bun.InsertQuery
values map[string]interface{}
}
func (b *BunInsertQuery) Model(model interface{}) InsertQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunInsertQuery) Table(table string) InsertQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunInsertQuery) Value(column string, value interface{}) InsertQuery {
if b.values == nil {
b.values = make(map[string]interface{})
}
b.values[column] = value
return b
}
func (b *BunInsertQuery) OnConflict(action string) InsertQuery {
b.query = b.query.On(action)
return b
}
func (b *BunInsertQuery) Returning(columns ...string) InsertQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
}
return b
}
func (b *BunInsertQuery) Exec(ctx context.Context) (Result, error) {
if b.values != nil {
// For Bun, we need to handle this differently
for k, v := range b.values {
b.query = b.query.Set("? = ?", bun.Ident(k), v)
}
}
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunUpdateQuery implements UpdateQuery for Bun
type BunUpdateQuery struct {
query *bun.UpdateQuery
}
func (b *BunUpdateQuery) Model(model interface{}) UpdateQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunUpdateQuery) Table(table string) UpdateQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunUpdateQuery) Set(column string, value interface{}) UpdateQuery {
b.query = b.query.Set(column+" = ?", value)
return b
}
func (b *BunUpdateQuery) SetMap(values map[string]interface{}) UpdateQuery {
for column, value := range values {
b.query = b.query.Set(column+" = ?", value)
}
return b
}
func (b *BunUpdateQuery) Where(query string, args ...interface{}) UpdateQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunUpdateQuery) Returning(columns ...string) UpdateQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
}
return b
}
func (b *BunUpdateQuery) Exec(ctx context.Context) (Result, error) {
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunDeleteQuery implements DeleteQuery for Bun
type BunDeleteQuery struct {
query *bun.DeleteQuery
}
func (b *BunDeleteQuery) Model(model interface{}) DeleteQuery {
b.query = b.query.Model(model)
return b
}
func (b *BunDeleteQuery) Table(table string) DeleteQuery {
b.query = b.query.Table(table)
return b
}
func (b *BunDeleteQuery) Where(query string, args ...interface{}) DeleteQuery {
b.query = b.query.Where(query, args...)
return b
}
func (b *BunDeleteQuery) Exec(ctx context.Context) (Result, error) {
result, err := b.query.Exec(ctx)
return &BunResult{result: result}, err
}
// BunResult implements Result for Bun
type BunResult struct {
result sql.Result
}
func (b *BunResult) RowsAffected() int64 {
if b.result == nil {
return 0
}
rows, _ := b.result.RowsAffected()
return rows
}
func (b *BunResult) LastInsertId() (int64, error) {
if b.result == nil {
return 0, nil
}
return b.result.LastInsertId()
}
// BunTxAdapter wraps a Bun transaction to implement the Database interface
type BunTxAdapter struct {
tx bun.Tx
}
func (b *BunTxAdapter) NewSelect() SelectQuery {
return &BunSelectQuery{query: b.tx.NewSelect()}
}
func (b *BunTxAdapter) NewInsert() InsertQuery {
return &BunInsertQuery{query: b.tx.NewInsert()}
}
func (b *BunTxAdapter) NewUpdate() UpdateQuery {
return &BunUpdateQuery{query: b.tx.NewUpdate()}
}
func (b *BunTxAdapter) NewDelete() DeleteQuery {
return &BunDeleteQuery{query: b.tx.NewDelete()}
}
func (b *BunTxAdapter) Exec(ctx context.Context, query string, args ...interface{}) (Result, error) {
result, err := b.tx.ExecContext(ctx, query, args...)
return &BunResult{result: result}, err
}
func (b *BunTxAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return b.tx.NewRaw(query, args...).Scan(ctx, dest)
}
func (b *BunTxAdapter) BeginTx(ctx context.Context) (Database, error) {
return nil, fmt.Errorf("nested transactions not supported")
}
func (b *BunTxAdapter) CommitTx(ctx context.Context) error {
return b.tx.Commit()
}
func (b *BunTxAdapter) RollbackTx(ctx context.Context) error {
return b.tx.Rollback()
}
func (b *BunTxAdapter) RunInTransaction(ctx context.Context, fn func(Database) error) error {
return fn(b) // Already in transaction
}

View File

@@ -1,218 +0,0 @@
package resolvespec
import (
"net/http"
"github.com/uptrace/bunrouter"
)
// BunRouterAdapter adapts uptrace/bunrouter to work with our Router interface
type BunRouterAdapter struct {
router *bunrouter.Router
}
// NewBunRouterAdapter creates a new bunrouter adapter
func NewBunRouterAdapter(router *bunrouter.Router) *BunRouterAdapter {
return &BunRouterAdapter{router: router}
}
// NewBunRouterAdapterDefault creates a new bunrouter adapter with default router
func NewBunRouterAdapterDefault() *BunRouterAdapter {
return &BunRouterAdapter{router: bunrouter.New()}
}
func (b *BunRouterAdapter) HandleFunc(pattern string, handler HTTPHandlerFunc) RouteRegistration {
route := &BunRouterRegistration{
router: b.router,
pattern: pattern,
handler: handler,
}
return route
}
func (b *BunRouterAdapter) ServeHTTP(w ResponseWriter, r Request) {
// This method would be used when we need to serve through our interface
// For now, we'll work directly with the underlying router
panic("ServeHTTP not implemented - use GetBunRouter() for direct access")
}
// GetBunRouter returns the underlying bunrouter for direct access
func (b *BunRouterAdapter) GetBunRouter() *bunrouter.Router {
return b.router
}
// BunRouterRegistration implements RouteRegistration for bunrouter
type BunRouterRegistration struct {
router *bunrouter.Router
pattern string
handler HTTPHandlerFunc
}
func (b *BunRouterRegistration) Methods(methods ...string) RouteRegistration {
// bunrouter handles methods differently - we'll register for each method
for _, method := range methods {
b.router.Handle(method, b.pattern, func(w http.ResponseWriter, req bunrouter.Request) error {
// Convert bunrouter.Request to our HTTPRequest
reqAdapter := &BunRouterRequest{req: req}
respAdapter := NewHTTPResponseWriter(w)
b.handler(respAdapter, reqAdapter)
return nil
})
}
return b
}
func (b *BunRouterRegistration) PathPrefix(prefix string) RouteRegistration {
// bunrouter doesn't have PathPrefix like mux, but we can modify the pattern
newPattern := prefix + b.pattern
b.pattern = newPattern
return b
}
// BunRouterRequest adapts bunrouter.Request to our Request interface
type BunRouterRequest struct {
req bunrouter.Request
body []byte
}
func (b *BunRouterRequest) Method() string {
return b.req.Method
}
func (b *BunRouterRequest) URL() string {
return b.req.URL.String()
}
func (b *BunRouterRequest) Header(key string) string {
return b.req.Header.Get(key)
}
func (b *BunRouterRequest) Body() ([]byte, error) {
if b.body != nil {
return b.body, nil
}
if b.req.Body == nil {
return nil, nil
}
// Create HTTPRequest adapter and use its Body() method
httpAdapter := NewHTTPRequest(b.req.Request)
body, err := httpAdapter.Body()
if err != nil {
return nil, err
}
b.body = body
return body, nil
}
func (b *BunRouterRequest) PathParam(key string) string {
return b.req.Param(key)
}
func (b *BunRouterRequest) QueryParam(key string) string {
return b.req.URL.Query().Get(key)
}
// StandardBunRouterAdapter creates routes compatible with standard bunrouter handlers
type StandardBunRouterAdapter struct {
*BunRouterAdapter
}
func NewStandardBunRouterAdapter() *StandardBunRouterAdapter {
return &StandardBunRouterAdapter{
BunRouterAdapter: NewBunRouterAdapterDefault(),
}
}
// RegisterRoute registers a route that works with the existing APIHandler
func (s *StandardBunRouterAdapter) RegisterRoute(method, pattern string, handler func(http.ResponseWriter, *http.Request, map[string]string)) {
s.router.Handle(method, pattern, func(w http.ResponseWriter, req bunrouter.Request) error {
// Extract path parameters
params := make(map[string]string)
// bunrouter doesn't provide a direct way to get all params
// You would typically access them individually with req.Param("name")
// For this example, we'll create the map based on the request context
handler(w, req.Request, params)
return nil
})
}
// RegisterRouteWithParams registers a route with explicit parameter extraction
func (s *StandardBunRouterAdapter) RegisterRouteWithParams(method, pattern string, paramNames []string, handler func(http.ResponseWriter, *http.Request, map[string]string)) {
s.router.Handle(method, pattern, func(w http.ResponseWriter, req bunrouter.Request) error {
// Extract specified path parameters
params := make(map[string]string)
for _, paramName := range paramNames {
params[paramName] = req.Param(paramName)
}
handler(w, req.Request, params)
return nil
})
}
// BunRouterConfig holds bunrouter-specific configuration
type BunRouterConfig struct {
UseStrictSlash bool
RedirectTrailingSlash bool
HandleMethodNotAllowed bool
HandleOPTIONS bool
GlobalOPTIONS http.Handler
GlobalMethodNotAllowed http.Handler
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}
// DefaultBunRouterConfig returns default bunrouter configuration
func DefaultBunRouterConfig() *BunRouterConfig {
return &BunRouterConfig{
UseStrictSlash: false,
RedirectTrailingSlash: true,
HandleMethodNotAllowed: true,
HandleOPTIONS: true,
}
}
// SetupBunRouterWithResolveSpec sets up bunrouter routes for ResolveSpec
func SetupBunRouterWithResolveSpec(router *bunrouter.Router, handler *APIHandlerCompat) {
// Setup standard ResolveSpec routes with bunrouter
router.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
handler.Handle(w, req.Request, params)
return nil
})
router.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
"id": req.Param("id"),
}
handler.Handle(w, req.Request, params)
return nil
})
router.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
handler.HandleGet(w, req.Request, params)
return nil
})
router.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
"id": req.Param("id"),
}
handler.HandleGet(w, req.Request, params)
return nil
})
}

View File

@@ -1,72 +0,0 @@
package resolvespec
import (
"net/http"
"github.com/Warky-Devs/ResolveSpec/pkg/models"
"gorm.io/gorm"
)
// NewAPIHandler creates a new APIHandler instance (backward compatibility)
// For now, this returns the legacy APIHandler to maintain full compatibility
// including preloading functionality. Users can opt-in to new abstractions when ready.
func NewAPIHandler(db *gorm.DB) *APIHandlerCompat {
legacyHandler := NewLegacyAPIHandler(db)
// Initialize new abstractions for future use
gormAdapter := NewGormAdapter(db)
registry := NewModelRegistry()
// Initialize registry with existing models
models.IterateModels(func(name string, model interface{}) {
registry.RegisterModel(name, model)
})
newHandler := NewHandler(gormAdapter, registry)
return &APIHandlerCompat{
legacyHandler: legacyHandler,
newHandler: newHandler,
db: db,
}
}
// APIHandlerCompat provides backward compatibility with the original APIHandler
type APIHandlerCompat struct {
legacyHandler *LegacyAPIHandler // For full backward compatibility
newHandler *Handler // New abstracted handler (optional use)
db *gorm.DB // Legacy GORM reference
}
// Handle maintains the original signature for backward compatibility
func (a *APIHandlerCompat) Handle(w http.ResponseWriter, r *http.Request, params map[string]string) {
// Use legacy handler to maintain full compatibility including preloading
a.legacyHandler.Handle(w, r, params)
}
// HandleGet maintains the original signature for backward compatibility
func (a *APIHandlerCompat) HandleGet(w http.ResponseWriter, r *http.Request, params map[string]string) {
// Use legacy handler for metadata
a.legacyHandler.HandleGet(w, r, params)
}
// RegisterModel maintains the original signature for backward compatibility
func (a *APIHandlerCompat) RegisterModel(schema, name string, model interface{}) error {
// Register with both legacy handler and new handler
err1 := a.legacyHandler.RegisterModel(schema, name, model)
err2 := a.newHandler.RegisterModel(schema, name, model)
if err1 != nil {
return err1
}
return err2
}
// GetNewHandler returns the new abstracted handler for advanced use cases
func (a *APIHandlerCompat) GetNewHandler() *Handler {
return a.newHandler
}
// GetLegacyHandler returns the legacy handler for cases needing full GORM features
func (a *APIHandlerCompat) GetLegacyHandler() *LegacyAPIHandler {
return a.legacyHandler
}

View File

@@ -1,250 +0,0 @@
package resolvespec
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
"gorm.io/gorm"
)
// Read handler
func (h *LegacyAPIHandler) handleRead(w http.ResponseWriter, r *http.Request, schema, entity, id string, options RequestOptions) {
logger.Info("Reading records from %s.%s", schema, entity)
// Get the model struct for the entity
model, err := h.getModelForEntity(schema, entity)
if err != nil {
logger.Error("Invalid entity: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
return
}
GormTableNameInterface, ok := model.(GormTableNameInterface)
if !ok {
logger.Error("Model does not implement GormTableNameInterface")
h.sendError(w, http.StatusInternalServerError, "model_error", "Model does not implement GormTableNameInterface", nil)
return
}
query := h.db.Model(model).Table(GormTableNameInterface.TableName())
// Apply column selection
if len(options.Columns) > 0 {
logger.Debug("Selecting columns: %v", options.Columns)
query = query.Select(options.Columns)
}
// Apply preloading
for _, preload := range options.Preload {
logger.Debug("Applying preload for relation: %s", preload.Relation)
query = query.Preload(preload.Relation, func(db *gorm.DB) *gorm.DB {
if len(preload.Columns) > 0 {
db = db.Select(preload.Columns)
}
if len(preload.Filters) > 0 {
for _, filter := range preload.Filters {
db = h.applyFilter(db, filter)
}
}
return db
})
}
// Apply filters
for _, filter := range options.Filters {
logger.Debug("Applying filter: %s %s %v", filter.Column, filter.Operator, filter.Value)
query = h.applyFilter(query, filter)
}
// Apply sorting
for _, sort := range options.Sort {
direction := "ASC"
if strings.ToLower(sort.Direction) == "desc" {
direction = "DESC"
}
logger.Debug("Applying sort: %s %s", sort.Column, direction)
query = query.Order(fmt.Sprintf("%s %s", sort.Column, direction))
}
// Get total count before pagination
var total int64
if err := query.Count(&total).Error; err != nil {
logger.Error("Error counting records: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error counting records", err)
return
}
logger.Debug("Total records before filtering: %d", total)
// Apply pagination
if options.Limit != nil && *options.Limit > 0 {
logger.Debug("Applying limit: %d", *options.Limit)
query = query.Limit(*options.Limit)
}
if options.Offset != nil && *options.Offset > 0 {
logger.Debug("Applying offset: %d", *options.Offset)
query = query.Offset(*options.Offset)
}
// Execute query
var result interface{}
if id != "" {
logger.Debug("Querying single record with ID: %s", id)
singleResult := model
if err := query.First(singleResult, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warn("Record not found with ID: %s", id)
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
return
}
logger.Error("Error querying record: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
return
}
result = singleResult
} else {
logger.Debug("Querying multiple records")
sliceType := reflect.SliceOf(reflect.TypeOf(model))
results := reflect.New(sliceType).Interface()
if err := query.Find(results).Error; err != nil {
logger.Error("Error querying records: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
return
}
result = reflect.ValueOf(results).Elem().Interface()
}
logger.Info("Successfully retrieved records")
h.sendResponse(w, result, &Metadata{
Total: total,
Filtered: total,
Limit: optionalInt(options.Limit),
Offset: optionalInt(options.Offset),
})
}
// Create handler
func (h *LegacyAPIHandler) handleCreate(w http.ResponseWriter, r *http.Request, schema, entity string, data any, options RequestOptions) {
logger.Info("Creating records for %s.%s", schema, entity)
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
switch v := data.(type) {
case map[string]interface{}:
result := query.Create(v)
if result.Error != nil {
logger.Error("Error creating record: %v", result.Error)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", result.Error)
return
}
logger.Info("Successfully created record")
h.sendResponse(w, v, nil)
case []map[string]interface{}:
result := query.Create(v)
if result.Error != nil {
logger.Error("Error creating records: %v", result.Error)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", result.Error)
return
}
logger.Info("Successfully created %d records", len(v))
h.sendResponse(w, v, nil)
case []interface{}:
list := make([]interface{}, 0)
for _, item := range v {
result := query.Create(item)
list = append(list, item)
if result.Error != nil {
logger.Error("Error creating records: %v", result.Error)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", result.Error)
return
}
logger.Info("Successfully created %d records", len(v))
}
h.sendResponse(w, list, nil)
default:
logger.Error("Invalid data type for create operation: %T", data)
}
}
// Update handler
func (h *LegacyAPIHandler) handleUpdate(w http.ResponseWriter, r *http.Request, schema, entity string, urlID string, reqID any, data any, options RequestOptions) {
logger.Info("Updating records for %s.%s", schema, entity)
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
switch {
case urlID != "":
logger.Debug("Updating by URL ID: %s", urlID)
result := query.Where("id = ?", urlID).Updates(data)
handleUpdateResult(w, h, result, data)
case reqID != nil:
switch id := reqID.(type) {
case string:
logger.Debug("Updating by request ID: %s", id)
result := query.Where("id = ?", id).Updates(data)
handleUpdateResult(w, h, result, data)
case []string:
logger.Debug("Updating by multiple IDs: %v", id)
result := query.Where("id IN ?", id).Updates(data)
handleUpdateResult(w, h, result, data)
}
case data != nil:
switch v := data.(type) {
case []map[string]interface{}:
logger.Debug("Performing bulk update with %d records", len(v))
err := h.db.Transaction(func(tx *gorm.DB) error {
for _, item := range v {
if id, ok := item["id"].(string); ok {
if err := tx.Where("id = ?", id).Updates(item).Error; err != nil {
logger.Error("Error in bulk update transaction: %v", err)
return err
}
}
}
return nil
})
if err != nil {
h.sendError(w, http.StatusInternalServerError, "update_error", "Error in bulk update", err)
return
}
logger.Info("Bulk update completed successfully")
h.sendResponse(w, data, nil)
}
default:
logger.Error("Invalid data type for update operation: %T", data)
}
}
// Delete handler
func (h *LegacyAPIHandler) handleDelete(w http.ResponseWriter, r *http.Request, schema, entity, id string) {
logger.Info("Deleting records from %s.%s", schema, entity)
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
if id == "" {
logger.Error("Delete operation requires an ID")
h.sendError(w, http.StatusBadRequest, "missing_id", "Delete operation requires an ID", nil)
return
}
result := query.Delete("id = ?", id)
if result.Error != nil {
logger.Error("Error deleting record: %v", result.Error)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting record", result.Error)
return
}
if result.RowsAffected == 0 {
logger.Warn("No record found to delete with ID: %s", id)
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
return
}
logger.Info("Successfully deleted record with ID: %s", id)
h.sendResponse(w, nil, nil)
}

View File

@@ -1,135 +0,0 @@
package resolvespec
import "context"
// Database interface designed to work with both GORM and Bun
type Database interface {
// Core query operations
NewSelect() SelectQuery
NewInsert() InsertQuery
NewUpdate() UpdateQuery
NewDelete() DeleteQuery
// Raw SQL execution
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error
// Transaction support
BeginTx(ctx context.Context) (Database, error)
CommitTx(ctx context.Context) error
RollbackTx(ctx context.Context) error
RunInTransaction(ctx context.Context, fn func(Database) error) error
}
// SelectQuery interface for building SELECT queries (compatible with both GORM and Bun)
type SelectQuery interface {
Model(model interface{}) SelectQuery
Table(table string) SelectQuery
Column(columns ...string) SelectQuery
Where(query string, args ...interface{}) SelectQuery
WhereOr(query string, args ...interface{}) SelectQuery
Join(query string, args ...interface{}) SelectQuery
LeftJoin(query string, args ...interface{}) SelectQuery
Order(order string) SelectQuery
Limit(n int) SelectQuery
Offset(n int) SelectQuery
Group(group string) SelectQuery
Having(having string, args ...interface{}) SelectQuery
// Execution methods
Scan(ctx context.Context, dest interface{}) error
Count(ctx context.Context) (int, error)
Exists(ctx context.Context) (bool, error)
}
// InsertQuery interface for building INSERT queries
type InsertQuery interface {
Model(model interface{}) InsertQuery
Table(table string) InsertQuery
Value(column string, value interface{}) InsertQuery
OnConflict(action string) InsertQuery
Returning(columns ...string) InsertQuery
// Execution
Exec(ctx context.Context) (Result, error)
}
// UpdateQuery interface for building UPDATE queries
type UpdateQuery interface {
Model(model interface{}) UpdateQuery
Table(table string) UpdateQuery
Set(column string, value interface{}) UpdateQuery
SetMap(values map[string]interface{}) UpdateQuery
Where(query string, args ...interface{}) UpdateQuery
Returning(columns ...string) UpdateQuery
// Execution
Exec(ctx context.Context) (Result, error)
}
// DeleteQuery interface for building DELETE queries
type DeleteQuery interface {
Model(model interface{}) DeleteQuery
Table(table string) DeleteQuery
Where(query string, args ...interface{}) DeleteQuery
// Execution
Exec(ctx context.Context) (Result, error)
}
// Result interface for query execution results
type Result interface {
RowsAffected() int64
LastInsertId() (int64, error)
}
// ModelRegistry manages model registration and retrieval
type ModelRegistry interface {
RegisterModel(name string, model interface{}) error
GetModel(name string) (interface{}, error)
GetAllModels() map[string]interface{}
GetModelByEntity(schema, entity string) (interface{}, error)
}
// Router interface for HTTP router abstraction
type Router interface {
HandleFunc(pattern string, handler HTTPHandlerFunc) RouteRegistration
ServeHTTP(w ResponseWriter, r *Request)
}
// RouteRegistration allows method chaining for route configuration
type RouteRegistration interface {
Methods(methods ...string) RouteRegistration
PathPrefix(prefix string) RouteRegistration
}
// Request interface abstracts HTTP request
type Request interface {
Method() string
URL() string
Header(key string) string
Body() ([]byte, error)
PathParam(key string) string
QueryParam(key string) string
}
// ResponseWriter interface abstracts HTTP response
type ResponseWriter interface {
SetHeader(key, value string)
WriteHeader(statusCode int)
Write(data []byte) (int, error)
WriteJSON(data interface{}) error
}
// HTTPHandlerFunc type for HTTP handlers
type HTTPHandlerFunc func(ResponseWriter, Request)
// TableNameProvider interface for models that provide table names
type TableNameProvider interface {
TableName() string
}
// SchemaProvider interface for models that provide schema names
type SchemaProvider interface {
SchemaName() string
}

View File

@@ -1,282 +0,0 @@
package resolvespec
import (
"context"
"gorm.io/gorm"
)
// GormAdapter adapts GORM to work with our Database interface
type GormAdapter struct {
db *gorm.DB
}
// NewGormAdapter creates a new GORM adapter
func NewGormAdapter(db *gorm.DB) *GormAdapter {
return &GormAdapter{db: db}
}
func (g *GormAdapter) NewSelect() SelectQuery {
return &GormSelectQuery{db: g.db}
}
func (g *GormAdapter) NewInsert() InsertQuery {
return &GormInsertQuery{db: g.db}
}
func (g *GormAdapter) NewUpdate() UpdateQuery {
return &GormUpdateQuery{db: g.db}
}
func (g *GormAdapter) NewDelete() DeleteQuery {
return &GormDeleteQuery{db: g.db}
}
func (g *GormAdapter) Exec(ctx context.Context, query string, args ...interface{}) (Result, error) {
result := g.db.WithContext(ctx).Exec(query, args...)
return &GormResult{result: result}, result.Error
}
func (g *GormAdapter) Query(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return g.db.WithContext(ctx).Raw(query, args...).Find(dest).Error
}
func (g *GormAdapter) BeginTx(ctx context.Context) (Database, error) {
tx := g.db.WithContext(ctx).Begin()
if tx.Error != nil {
return nil, tx.Error
}
return &GormAdapter{db: tx}, nil
}
func (g *GormAdapter) CommitTx(ctx context.Context) error {
return g.db.WithContext(ctx).Commit().Error
}
func (g *GormAdapter) RollbackTx(ctx context.Context) error {
return g.db.WithContext(ctx).Rollback().Error
}
func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(Database) error) error {
return g.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
adapter := &GormAdapter{db: tx}
return fn(adapter)
})
}
// GormSelectQuery implements SelectQuery for GORM
type GormSelectQuery struct {
db *gorm.DB
}
func (g *GormSelectQuery) Model(model interface{}) SelectQuery {
g.db = g.db.Model(model)
return g
}
func (g *GormSelectQuery) Table(table string) SelectQuery {
g.db = g.db.Table(table)
return g
}
func (g *GormSelectQuery) Column(columns ...string) SelectQuery {
g.db = g.db.Select(columns)
return g
}
func (g *GormSelectQuery) Where(query string, args ...interface{}) SelectQuery {
g.db = g.db.Where(query, args...)
return g
}
func (g *GormSelectQuery) WhereOr(query string, args ...interface{}) SelectQuery {
g.db = g.db.Or(query, args...)
return g
}
func (g *GormSelectQuery) Join(query string, args ...interface{}) SelectQuery {
g.db = g.db.Joins(query, args...)
return g
}
func (g *GormSelectQuery) LeftJoin(query string, args ...interface{}) SelectQuery {
g.db = g.db.Joins("LEFT JOIN "+query, args...)
return g
}
func (g *GormSelectQuery) Order(order string) SelectQuery {
g.db = g.db.Order(order)
return g
}
func (g *GormSelectQuery) Limit(n int) SelectQuery {
g.db = g.db.Limit(n)
return g
}
func (g *GormSelectQuery) Offset(n int) SelectQuery {
g.db = g.db.Offset(n)
return g
}
func (g *GormSelectQuery) Group(group string) SelectQuery {
g.db = g.db.Group(group)
return g
}
func (g *GormSelectQuery) Having(having string, args ...interface{}) SelectQuery {
g.db = g.db.Having(having, args...)
return g
}
func (g *GormSelectQuery) Scan(ctx context.Context, dest interface{}) error {
return g.db.WithContext(ctx).Find(dest).Error
}
func (g *GormSelectQuery) Count(ctx context.Context) (int, error) {
var count int64
err := g.db.WithContext(ctx).Count(&count).Error
return int(count), err
}
func (g *GormSelectQuery) Exists(ctx context.Context) (bool, error) {
var count int64
err := g.db.WithContext(ctx).Limit(1).Count(&count).Error
return count > 0, err
}
// GormInsertQuery implements InsertQuery for GORM
type GormInsertQuery struct {
db *gorm.DB
model interface{}
values map[string]interface{}
}
func (g *GormInsertQuery) Model(model interface{}) InsertQuery {
g.model = model
g.db = g.db.Model(model)
return g
}
func (g *GormInsertQuery) Table(table string) InsertQuery {
g.db = g.db.Table(table)
return g
}
func (g *GormInsertQuery) Value(column string, value interface{}) InsertQuery {
if g.values == nil {
g.values = make(map[string]interface{})
}
g.values[column] = value
return g
}
func (g *GormInsertQuery) OnConflict(action string) InsertQuery {
// GORM handles conflicts differently, this would need specific implementation
return g
}
func (g *GormInsertQuery) Returning(columns ...string) InsertQuery {
// GORM doesn't have explicit RETURNING, but updates the model
return g
}
func (g *GormInsertQuery) Exec(ctx context.Context) (Result, error) {
var result *gorm.DB
if g.model != nil {
result = g.db.WithContext(ctx).Create(g.model)
} else if g.values != nil {
result = g.db.WithContext(ctx).Create(g.values)
} else {
result = g.db.WithContext(ctx).Create(map[string]interface{}{})
}
return &GormResult{result: result}, result.Error
}
// GormUpdateQuery implements UpdateQuery for GORM
type GormUpdateQuery struct {
db *gorm.DB
model interface{}
updates interface{}
}
func (g *GormUpdateQuery) Model(model interface{}) UpdateQuery {
g.model = model
g.db = g.db.Model(model)
return g
}
func (g *GormUpdateQuery) Table(table string) UpdateQuery {
g.db = g.db.Table(table)
return g
}
func (g *GormUpdateQuery) Set(column string, value interface{}) UpdateQuery {
if g.updates == nil {
g.updates = make(map[string]interface{})
}
if updates, ok := g.updates.(map[string]interface{}); ok {
updates[column] = value
}
return g
}
func (g *GormUpdateQuery) SetMap(values map[string]interface{}) UpdateQuery {
g.updates = values
return g
}
func (g *GormUpdateQuery) Where(query string, args ...interface{}) UpdateQuery {
g.db = g.db.Where(query, args...)
return g
}
func (g *GormUpdateQuery) Returning(columns ...string) UpdateQuery {
// GORM doesn't have explicit RETURNING
return g
}
func (g *GormUpdateQuery) Exec(ctx context.Context) (Result, error) {
result := g.db.WithContext(ctx).Updates(g.updates)
return &GormResult{result: result}, result.Error
}
// GormDeleteQuery implements DeleteQuery for GORM
type GormDeleteQuery struct {
db *gorm.DB
model interface{}
}
func (g *GormDeleteQuery) Model(model interface{}) DeleteQuery {
g.model = model
g.db = g.db.Model(model)
return g
}
func (g *GormDeleteQuery) Table(table string) DeleteQuery {
g.db = g.db.Table(table)
return g
}
func (g *GormDeleteQuery) Where(query string, args ...interface{}) DeleteQuery {
g.db = g.db.Where(query, args...)
return g
}
func (g *GormDeleteQuery) Exec(ctx context.Context) (Result, error) {
result := g.db.WithContext(ctx).Delete(g.model)
return &GormResult{result: result}, result.Error
}
// GormResult implements Result for GORM
type GormResult struct {
result *gorm.DB
}
func (g *GormResult) RowsAffected() int64 {
return g.result.RowsAffected
}
func (g *GormResult) LastInsertId() (int64, error) {
// GORM doesn't directly provide last insert ID, would need specific implementation
return 0, nil
}

View File

@@ -8,17 +8,18 @@ import (
"reflect"
"strings"
"github.com/Warky-Devs/ResolveSpec/pkg/common"
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
)
// Handler handles API requests using database and model abstractions
type Handler struct {
db Database
registry ModelRegistry
db common.Database
registry common.ModelRegistry
}
// NewHandler creates a new API handler with database and registry abstractions
func NewHandler(db Database, registry ModelRegistry) *Handler {
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
return &Handler{
db: db,
registry: registry,
@@ -26,9 +27,9 @@ func NewHandler(db Database, registry ModelRegistry) *Handler {
}
// Handle processes API requests through router-agnostic interface
func (h *Handler) Handle(w ResponseWriter, r Request, params map[string]string) {
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
ctx := context.Background()
body, err := r.Body()
if err != nil {
logger.Error("Failed to read request body: %v", err)
@@ -36,7 +37,7 @@ func (h *Handler) Handle(w ResponseWriter, r Request, params map[string]string)
return
}
var req RequestBody
var req common.RequestBody
if err := json.Unmarshal(body, &req); err != nil {
logger.Error("Failed to decode request body: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
@@ -65,7 +66,7 @@ func (h *Handler) Handle(w ResponseWriter, r Request, params map[string]string)
}
// HandleGet processes GET requests for metadata
func (h *Handler) HandleGet(w ResponseWriter, r Request, params map[string]string) {
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
schema := params["schema"]
entity := params["entity"]
@@ -82,7 +83,7 @@ func (h *Handler) HandleGet(w ResponseWriter, r Request, params map[string]strin
h.sendResponse(w, metadata, nil)
}
func (h *Handler) handleRead(ctx context.Context, w ResponseWriter, schema, entity, id string, options RequestOptions) {
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schema, entity, id string, options common.RequestOptions) {
logger.Info("Reading records from %s.%s", schema, entity)
model, err := h.registry.GetModelByEntity(schema, entity)
@@ -104,11 +105,9 @@ func (h *Handler) handleRead(ctx context.Context, w ResponseWriter, schema, enti
query = query.Column(options.Columns...)
}
// Note: Preloading is not implemented in the new database abstraction yet
// This is a limitation of the current interface design
// For now, preloading should use the legacy APIHandler
// Apply preloading
if len(options.Preload) > 0 {
logger.Warn("Preloading not yet implemented in new handler - use legacy APIHandler for preload functionality")
query = h.applyPreloads(model, query, options.Preload)
}
// Apply filters
@@ -172,18 +171,35 @@ func (h *Handler) handleRead(ctx context.Context, w ResponseWriter, schema, enti
}
logger.Info("Successfully retrieved records")
h.sendResponse(w, result, &Metadata{
limit := 0
if options.Limit != nil {
limit = *options.Limit
}
offset := 0
if options.Offset != nil {
offset = *options.Offset
}
h.sendResponse(w, result, &common.Metadata{
Total: int64(total),
Filtered: int64(total),
Limit: optionalInt(options.Limit),
Offset: optionalInt(options.Offset),
Limit: limit,
Offset: offset,
})
}
func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, entity string, data interface{}, options RequestOptions) {
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, schema, entity string, data interface{}, options common.RequestOptions) {
logger.Info("Creating records for %s.%s", schema, entity)
tableName := fmt.Sprintf("%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) {
@@ -201,7 +217,7 @@ func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, en
h.sendResponse(w, v, nil)
case []map[string]interface{}:
err := h.db.RunInTransaction(ctx, func(tx Database) error {
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
txQuery := tx.NewInsert().Table(tableName)
for key, value := range item {
@@ -224,7 +240,7 @@ func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, en
case []interface{}:
// Handle []interface{} type from JSON unmarshaling
list := make([]interface{}, 0)
err := h.db.RunInTransaction(ctx, func(tx Database) error {
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
if itemMap, ok := item.(map[string]interface{}); ok {
txQuery := tx.NewInsert().Table(tableName)
@@ -253,10 +269,18 @@ func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, en
}
}
func (h *Handler) handleUpdate(ctx context.Context, w ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options RequestOptions) {
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) {
logger.Info("Updating records for %s.%s", schema, entity)
tableName := fmt.Sprintf("%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) {
@@ -289,18 +313,18 @@ func (h *Handler) handleUpdate(ctx context.Context, w ResponseWriter, schema, en
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", err)
return
}
if result.RowsAffected() == 0 {
logger.Warn("No records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
return
}
logger.Info("Successfully updated %d records", result.RowsAffected())
h.sendResponse(w, data, nil)
}
func (h *Handler) handleDelete(ctx context.Context, w ResponseWriter, schema, entity, id string) {
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, schema, entity, id string) {
logger.Info("Deleting records from %s.%s", schema, entity)
if id == "" {
@@ -309,7 +333,14 @@ func (h *Handler) handleDelete(ctx context.Context, w ResponseWriter, schema, en
return
}
tableName := fmt.Sprintf("%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.NewDelete().Table(tableName).Where("id = ?", id)
result, err := query.Exec(ctx)
@@ -318,7 +349,7 @@ func (h *Handler) handleDelete(ctx context.Context, w ResponseWriter, schema, en
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting record", err)
return
}
if result.RowsAffected() == 0 {
logger.Warn("No record found to delete with ID: %s", id)
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
@@ -329,7 +360,7 @@ func (h *Handler) handleDelete(ctx context.Context, w ResponseWriter, schema, en
h.sendResponse(w, nil, nil)
}
func (h *Handler) applyFilter(query SelectQuery, filter FilterOption) SelectQuery {
func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOption) common.SelectQuery {
switch filter.Operator {
case "eq":
return query.Where(fmt.Sprintf("%s = ?", filter.Column), filter.Value)
@@ -355,22 +386,22 @@ func (h *Handler) applyFilter(query SelectQuery, filter FilterOption) SelectQuer
}
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
if provider, ok := model.(TableNameProvider); ok {
if provider, ok := model.(common.TableNameProvider); ok {
return provider.TableName()
}
return fmt.Sprintf("%s.%s", schema, entity)
}
func (h *Handler) generateMetadata(schema, entity string, model interface{}) TableMetadata {
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
modelType := reflect.TypeOf(model)
if modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem()
}
metadata := TableMetadata{
metadata := &common.TableMetadata{
Schema: schema,
Table: entity,
Columns: make([]Column, 0),
Columns: make([]common.Column, 0),
Relations: make([]string, 0),
}
@@ -400,7 +431,7 @@ func (h *Handler) generateMetadata(schema, entity string, model interface{}) Tab
continue
}
column := Column{
column := common.Column{
Name: jsonName,
Type: getColumnType(field),
IsNullable: isNullable(field),
@@ -415,21 +446,21 @@ func (h *Handler) generateMetadata(schema, entity string, model interface{}) Tab
return metadata
}
func (h *Handler) sendResponse(w ResponseWriter, data interface{}, metadata *Metadata) {
func (h *Handler) sendResponse(w common.ResponseWriter, data interface{}, metadata *common.Metadata) {
w.SetHeader("Content-Type", "application/json")
w.WriteJSON(Response{
w.WriteJSON(common.Response{
Success: true,
Data: data,
Metadata: metadata,
})
}
func (h *Handler) sendError(w ResponseWriter, status int, code, message string, details interface{}) {
func (h *Handler) sendError(w common.ResponseWriter, status int, code, message string, details interface{}) {
w.SetHeader("Content-Type", "application/json")
w.WriteHeader(status)
w.WriteJSON(Response{
w.WriteJSON(common.Response{
Success: false,
Error: &APIError{
Error: &common.APIError{
Code: code,
Message: message,
Details: details,
@@ -442,4 +473,142 @@ func (h *Handler) sendError(w ResponseWriter, status int, code, message string,
func (h *Handler) RegisterModel(schema, name string, model interface{}) error {
fullname := fmt.Sprintf("%s.%s", schema, name)
return h.registry.RegisterModel(fullname, model)
}
}
// Helper functions
func getColumnType(field reflect.StructField) string {
// Check GORM type tag first
gormTag := field.Tag.Get("gorm")
if strings.Contains(gormTag, "type:") {
parts := strings.Split(gormTag, "type:")
if len(parts) > 1 {
typePart := strings.Split(parts[1], ";")[0]
return typePart
}
}
// Map Go types to SQL types
switch field.Type.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int32:
return "integer"
case reflect.Int64:
return "bigint"
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Bool:
return "boolean"
default:
if field.Type.Name() == "Time" {
return "timestamp"
}
return "unknown"
}
}
func isNullable(field reflect.StructField) bool {
// Check if it's a pointer type
if field.Type.Kind() == reflect.Ptr {
return true
}
// Check if it's a null type from sql package
typeName := field.Type.Name()
if strings.HasPrefix(typeName, "Null") {
return true
}
// Check GORM tags
gormTag := field.Tag.Get("gorm")
return !strings.Contains(gormTag, "not null")
}
// Preload support functions
type relationshipInfo struct {
fieldName string
jsonName string
relationType string // "belongsTo", "hasMany", "hasOne", "many2many"
foreignKey string
references string
joinTable string
relatedModel interface{}
}
func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, preloads []common.PreloadOption) common.SelectQuery {
modelType := reflect.TypeOf(model)
if modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem()
}
for _, preload := range preloads {
logger.Debug("Processing preload for relation: %s", preload.Relation)
relInfo := h.getRelationshipInfo(modelType, preload.Relation)
if relInfo == nil {
logger.Warn("Relation %s not found in model", preload.Relation)
continue
}
// Use the field name (capitalized) for ORM preloading
// ORMs like GORM and Bun expect the struct field name, not the JSON name
relationFieldName := relInfo.fieldName
// For now, we'll preload without conditions
// TODO: Implement column selection and filtering for preloads
// This requires a more sophisticated approach with callbacks or query builders
query = query.Preload(relationFieldName)
logger.Debug("Applied Preload for relation: %s (field: %s)", preload.Relation, relationFieldName)
}
return query
}
func (h *Handler) getRelationshipInfo(modelType reflect.Type, relationName string) *relationshipInfo {
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
jsonTag := field.Tag.Get("json")
jsonName := strings.Split(jsonTag, ",")[0]
if jsonName == relationName {
gormTag := field.Tag.Get("gorm")
info := &relationshipInfo{
fieldName: field.Name,
jsonName: jsonName,
}
// Parse GORM tag to determine relationship type and keys
if strings.Contains(gormTag, "foreignKey") {
info.foreignKey = h.extractTagValue(gormTag, "foreignKey")
info.references = h.extractTagValue(gormTag, "references")
// Determine if it's belongsTo or hasMany/hasOne
if field.Type.Kind() == reflect.Slice {
info.relationType = "hasMany"
} else if field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct {
info.relationType = "belongsTo"
}
} else if strings.Contains(gormTag, "many2many") {
info.relationType = "many2many"
info.joinTable = h.extractTagValue(gormTag, "many2many")
}
return info
}
}
return nil
}
func (h *Handler) extractTagValue(tag, key string) string {
parts := strings.Split(tag, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, key+":") {
return strings.TrimPrefix(part, key+":")
}
}
return ""
}

View File

@@ -1,131 +0,0 @@
package resolvespec
import (
"net/http"
"reflect"
"strings"
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
)
func (h *LegacyAPIHandler) HandleGet(w http.ResponseWriter, r *http.Request, params map[string]string) {
schema := params["schema"]
entity := params["entity"]
logger.Info("Getting metadata for %s.%s", schema, entity)
// Get model for the entity
model, err := h.getModelForEntity(schema, entity)
if err != nil {
logger.Error("Failed to get model: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
return
}
modelType := reflect.TypeOf(model)
if modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem()
}
metadata := TableMetadata{
Schema: schema,
Table: entity,
Columns: make([]Column, 0),
Relations: make([]string, 0),
}
// Get field information using reflection
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Parse GORM tags
gormTag := field.Tag.Get("gorm")
jsonTag := field.Tag.Get("json")
// Skip if json tag is "-"
if jsonTag == "-" {
continue
}
// Get JSON field name
jsonName := strings.Split(jsonTag, ",")[0]
if jsonName == "" {
jsonName = field.Name
}
// Check if it's a relation
if field.Type.Kind() == reflect.Slice ||
(field.Type.Kind() == reflect.Struct && field.Type.Name() != "Time") {
metadata.Relations = append(metadata.Relations, jsonName)
continue
}
column := Column{
Name: jsonName,
Type: getColumnType(field),
IsNullable: isNullable(field),
IsPrimary: strings.Contains(gormTag, "primaryKey"),
IsUnique: strings.Contains(gormTag, "unique") || strings.Contains(gormTag, "uniqueIndex"),
HasIndex: strings.Contains(gormTag, "index") || strings.Contains(gormTag, "uniqueIndex"),
}
metadata.Columns = append(metadata.Columns, column)
}
h.sendResponse(w, metadata, nil)
}
func getColumnType(field reflect.StructField) string {
// Check GORM type tag first
gormTag := field.Tag.Get("gorm")
if strings.Contains(gormTag, "type:") {
parts := strings.Split(gormTag, "type:")
if len(parts) > 1 {
typePart := strings.Split(parts[1], ";")[0]
return typePart
}
}
// Map Go types to SQL types
switch field.Type.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int32:
return "integer"
case reflect.Int64:
return "bigint"
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Bool:
return "boolean"
default:
if field.Type.Name() == "Time" {
return "timestamp"
}
return "unknown"
}
}
func isNullable(field reflect.StructField) bool {
// Check if it's a pointer type
if field.Type.Kind() == reflect.Ptr {
return true
}
// Check if it's a null type from sql package
typeName := field.Type.Name()
if strings.HasPrefix(typeName, "Null") {
return true
}
// Check GORM tags
gormTag := field.Tag.Get("gorm")
return !strings.Contains(gormTag, "not null")
}

View File

@@ -1,65 +0,0 @@
package resolvespec
import (
"fmt"
"sync"
)
// DefaultModelRegistry implements ModelRegistry interface
type DefaultModelRegistry struct {
models map[string]interface{}
mutex sync.RWMutex
}
// NewModelRegistry creates a new model registry
func NewModelRegistry() *DefaultModelRegistry {
return &DefaultModelRegistry{
models: make(map[string]interface{}),
}
}
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)
}
r.models[name] = model
return nil
}
func (r *DefaultModelRegistry) GetModel(name string) (interface{}, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
model, exists := r.models[name]
if !exists {
return nil, fmt.Errorf("model %s not found", name)
}
return model, nil
}
func (r *DefaultModelRegistry) GetAllModels() map[string]interface{} {
r.mutex.RLock()
defer r.mutex.RUnlock()
result := make(map[string]interface{})
for k, v := range r.models {
result[k] = v
}
return result
}
func (r *DefaultModelRegistry) GetModelByEntity(schema, entity string) (interface{}, error) {
// Try full name first
fullName := fmt.Sprintf("%s.%s", schema, entity)
if model, err := r.GetModel(fullName); err == nil {
return model, nil
}
// Fallback to entity name only
return r.GetModel(entity)
}

View File

@@ -3,145 +3,179 @@ package resolvespec
import (
"net/http"
"github.com/Warky-Devs/ResolveSpec/pkg/common/adapters/database"
"github.com/Warky-Devs/ResolveSpec/pkg/common/adapters/router"
"github.com/Warky-Devs/ResolveSpec/pkg/modelregistry"
"github.com/gorilla/mux"
"github.com/uptrace/bun"
"github.com/uptrace/bunrouter"
"gorm.io/gorm"
)
// NewAPIHandler creates a new APIHandler with GORM (backward compatibility)
func NewAPIHandlerWithGORM(db *gorm.DB) *APIHandlerCompat {
return NewAPIHandler(db)
}
// NewHandlerWithGORM creates a new Handler with GORM adapter
func NewHandlerWithGORM(db *gorm.DB) *Handler {
gormAdapter := NewGormAdapter(db)
registry := NewModelRegistry()
gormAdapter := database.NewGormAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(gormAdapter, registry)
}
// NewStandardRouter creates a router with standard HTTP handlers
func NewStandardRouter() *StandardMuxAdapter {
return NewStandardMuxAdapter()
// NewHandlerWithBun creates a new Handler with Bun adapter
func NewHandlerWithBun(db *bun.DB) *Handler {
bunAdapter := database.NewBunAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(bunAdapter, registry)
}
// SetupRoutes sets up routes for the ResolveSpec API with backward compatibility
func SetupRoutes(router *mux.Router, handler *APIHandlerCompat) {
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
// NewStandardMuxRouter creates a router with standard Mux HTTP handlers
func NewStandardMuxRouter() *router.StandardMuxAdapter {
return router.NewStandardMuxAdapter()
}
// NewStandardBunRouter creates a router with standard BunRouter handlers
func NewStandardBunRouter() *router.StandardBunRouterAdapter {
return router.NewStandardBunRouterAdapter()
}
// SetupMuxRoutes sets up routes for the ResolveSpec API with Mux
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
handler.Handle(w, r, vars)
reqAdapter := router.NewHTTPRequest(r)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, vars)
}).Methods("POST")
router.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
muxRouter.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
handler.Handle(w, r, vars)
reqAdapter := router.NewHTTPRequest(r)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, vars)
}).Methods("POST")
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
handler.HandleGet(w, r, vars)
reqAdapter := router.NewHTTPRequest(r)
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, vars)
}).Methods("GET")
}
// Example usage functions for documentation:
// ExampleWithGORM shows how to use ResolveSpec with GORM (current default)
// ExampleWithGORM shows how to use ResolveSpec with GORM
func ExampleWithGORM(db *gorm.DB) {
// Create handler using GORM (backward compatible)
handler := NewAPIHandlerWithGORM(db)
// Create handler using GORM
handler := NewHandlerWithGORM(db)
// Setup router
router := mux.NewRouter()
SetupRoutes(router, handler)
muxRouter := mux.NewRouter()
SetupMuxRoutes(muxRouter, handler)
// Register models
// handler.RegisterModel("public", "users", &User{})
}
// ExampleWithNewAPI shows how to use the new abstracted API
func ExampleWithNewAPI(db *gorm.DB) {
// Create database adapter
dbAdapter := NewGormAdapter(db)
// Create model registry
registry := NewModelRegistry()
// registry.RegisterModel("public.users", &User{})
// Create handler with new API
handler := NewHandler(dbAdapter, registry)
// Create router adapter
routerAdapter := NewStandardRouter()
// Register routes using new API
routerAdapter.RegisterRoute("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
reqAdapter := NewHTTPRequest(r)
respAdapter := NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
})
}
// ExampleWithBun shows how to switch to Bun ORM
func ExampleWithBun(bunDB *bun.DB) {
// Create Bun adapter
dbAdapter := NewBunAdapter(bunDB)
dbAdapter := database.NewBunAdapter(bunDB)
// Create model registry
registry := NewModelRegistry()
registry := modelregistry.NewModelRegistry()
// registry.RegisterModel("public.users", &User{})
// Create handler
handler := NewHandler(dbAdapter, registry)
// Setup routes same as with GORM
router := NewStandardRouter()
router.RegisterRoute("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
reqAdapter := NewHTTPRequest(r)
respAdapter := NewHTTPResponseWriter(w)
// Setup routes
muxRouter := mux.NewRouter()
SetupMuxRoutes(muxRouter, handler)
}
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
r := bunRouter.GetBunRouter()
r.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
"id": req.Param("id"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
r.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
"id": req.Param("id"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
}
// ExampleWithBunRouter shows how to use bunrouter from uptrace
func ExampleWithBunRouter(db *gorm.DB) {
// Create handler (can use any database adapter)
handler := NewAPIHandler(db)
func ExampleWithBunRouter(bunDB *bun.DB) {
// Create handler with Bun adapter
handler := NewHandlerWithBun(bunDB)
// Create bunrouter
router := NewStandardBunRouterAdapter()
bunRouter := router.NewStandardBunRouterAdapter()
// Setup ResolveSpec routes with bunrouter
SetupBunRouterWithResolveSpec(router.GetBunRouter(), handler)
SetupBunRouterRoutes(bunRouter, handler)
// Start server
// http.ListenAndServe(":8080", router.GetBunRouter())
// http.ListenAndServe(":8080", bunRouter.GetBunRouter())
}
// ExampleBunRouterWithBunDB shows the full uptrace stack (bunrouter + Bun ORM)
func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
// Create Bun database adapter
dbAdapter := NewBunAdapter(bunDB)
dbAdapter := database.NewBunAdapter(bunDB)
// Create model registry
registry := NewModelRegistry()
registry := modelregistry.NewModelRegistry()
// registry.RegisterModel("public.users", &User{})
// Create handler with Bun
handler := NewHandler(dbAdapter, registry)
// Create compatibility wrapper for existing APIs
compatHandler := &APIHandlerCompat{
legacyHandler: nil, // No legacy handler needed
newHandler: handler,
db: nil, // No GORM dependency
}
// Create bunrouter
router := NewStandardBunRouterAdapter()
bunRouter := router.NewStandardBunRouterAdapter()
// Setup ResolveSpec routes
SetupBunRouterWithResolveSpec(router.GetBunRouter(), compatHandler)
SetupBunRouterRoutes(bunRouter, handler)
// This gives you the full uptrace stack: bunrouter + Bun ORM
// http.ListenAndServe(":8080", router.GetBunRouter())
}
// http.ListenAndServe(":8080", bunRouter.GetBunRouter())
}

View File

@@ -1,210 +0,0 @@
package resolvespec
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
)
// MuxAdapter adapts Gorilla Mux to work with our Router interface
type MuxAdapter struct {
router *mux.Router
}
// NewMuxAdapter creates a new Mux adapter
func NewMuxAdapter(router *mux.Router) *MuxAdapter {
return &MuxAdapter{router: router}
}
func (m *MuxAdapter) HandleFunc(pattern string, handler HTTPHandlerFunc) RouteRegistration {
route := &MuxRouteRegistration{
router: m.router,
pattern: pattern,
handler: handler,
}
return route
}
func (m *MuxAdapter) ServeHTTP(w ResponseWriter, r Request) {
// This method would be used when we need to serve through our interface
// For now, we'll work directly with the underlying router
panic("ServeHTTP not implemented - use GetMuxRouter() for direct access")
}
// MuxRouteRegistration implements RouteRegistration for Mux
type MuxRouteRegistration struct {
router *mux.Router
pattern string
handler HTTPHandlerFunc
route *mux.Route
}
func (m *MuxRouteRegistration) Methods(methods ...string) RouteRegistration {
if m.route == nil {
m.route = m.router.HandleFunc(m.pattern, func(w http.ResponseWriter, r *http.Request) {
reqAdapter := &HTTPRequest{req: r, vars: mux.Vars(r)}
respAdapter := &HTTPResponseWriter{resp: w}
m.handler(respAdapter, reqAdapter)
})
}
m.route.Methods(methods...)
return m
}
func (m *MuxRouteRegistration) PathPrefix(prefix string) RouteRegistration {
if m.route == nil {
m.route = m.router.HandleFunc(m.pattern, func(w http.ResponseWriter, r *http.Request) {
reqAdapter := &HTTPRequest{req: r, vars: mux.Vars(r)}
respAdapter := &HTTPResponseWriter{resp: w}
m.handler(respAdapter, reqAdapter)
})
}
m.route.PathPrefix(prefix)
return m
}
// HTTPRequest adapts standard http.Request to our Request interface
type HTTPRequest struct {
req *http.Request
vars map[string]string
body []byte
}
func NewHTTPRequest(r *http.Request) *HTTPRequest {
return &HTTPRequest{
req: r,
vars: make(map[string]string),
}
}
func (h *HTTPRequest) Method() string {
return h.req.Method
}
func (h *HTTPRequest) URL() string {
return h.req.URL.String()
}
func (h *HTTPRequest) Header(key string) string {
return h.req.Header.Get(key)
}
func (h *HTTPRequest) Body() ([]byte, error) {
if h.body != nil {
return h.body, nil
}
if h.req.Body == nil {
return nil, nil
}
defer h.req.Body.Close()
body, err := io.ReadAll(h.req.Body)
if err != nil {
return nil, err
}
h.body = body
return body, nil
}
func (h *HTTPRequest) PathParam(key string) string {
return h.vars[key]
}
func (h *HTTPRequest) QueryParam(key string) string {
return h.req.URL.Query().Get(key)
}
// HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter
type HTTPResponseWriter struct {
resp http.ResponseWriter
w ResponseWriter
status int
}
func NewHTTPResponseWriter(w http.ResponseWriter) *HTTPResponseWriter {
return &HTTPResponseWriter{resp: w}
}
func (h *HTTPResponseWriter) SetHeader(key, value string) {
h.resp.Header().Set(key, value)
}
func (h *HTTPResponseWriter) WriteHeader(statusCode int) {
h.status = statusCode
h.resp.WriteHeader(statusCode)
}
func (h *HTTPResponseWriter) Write(data []byte) (int, error) {
return h.resp.Write(data)
}
func (h *HTTPResponseWriter) WriteJSON(data interface{}) error {
h.SetHeader("Content-Type", "application/json")
return json.NewEncoder(h.resp).Encode(data)
}
// StandardMuxAdapter creates routes compatible with standard http.HandlerFunc
type StandardMuxAdapter struct {
*MuxAdapter
}
func NewStandardMuxAdapter() *StandardMuxAdapter {
return &StandardMuxAdapter{
MuxAdapter: NewMuxAdapter(mux.NewRouter()),
}
}
// RegisterRoute registers a route that works with the existing APIHandler
func (s *StandardMuxAdapter) RegisterRoute(pattern string, handler func(http.ResponseWriter, *http.Request, map[string]string)) *mux.Route {
return s.router.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
handler(w, r, vars)
})
}
// GetMuxRouter returns the underlying mux router for direct access
func (s *StandardMuxAdapter) GetMuxRouter() *mux.Router {
return s.router
}
// GinAdapter for future Gin support
type GinAdapter struct {
// This would be implemented when Gin support is needed
// engine *gin.Engine
}
// EchoAdapter for future Echo support
type EchoAdapter struct {
// This would be implemented when Echo support is needed
// echo *echo.Echo
}
// PathParamExtractor extracts path parameters from different router types
type PathParamExtractor interface {
ExtractParams(*http.Request) map[string]string
}
// MuxParamExtractor extracts parameters from Gorilla Mux
type MuxParamExtractor struct{}
func (m MuxParamExtractor) ExtractParams(r *http.Request) map[string]string {
return mux.Vars(r)
}
// RouterConfig holds router configuration
type RouterConfig struct {
PathPrefix string
Middleware []func(http.Handler) http.Handler
ParamExtractor PathParamExtractor
}
// DefaultRouterConfig returns default router configuration
func DefaultRouterConfig() *RouterConfig {
return &RouterConfig{
PathPrefix: "",
Middleware: make([]func(http.Handler) http.Handler, 0),
ParamExtractor: MuxParamExtractor{},
}
}

View File

@@ -1,96 +0,0 @@
package resolvespec
type RequestBody struct {
Operation string `json:"operation"`
Data interface{} `json:"data"`
ID *int64 `json:"id"`
Options RequestOptions `json:"options"`
}
type RequestOptions struct {
Preload []PreloadOption `json:"preload"`
Columns []string `json:"columns"`
OmitColumns []string `json:"omit_columns"`
Filters []FilterOption `json:"filters"`
Sort []SortOption `json:"sort"`
Limit *int `json:"limit"`
Offset *int `json:"offset"`
CustomOperators []CustomOperator `json:"customOperators"`
ComputedColumns []ComputedColumn `json:"computedColumns"`
Parameters []Parameter `json:"parameters"`
}
type Parameter struct {
Name string `json:"name"`
Value string `json:"value"`
Sequence *int `json:"sequence"`
}
type PreloadOption struct {
Relation string `json:"relation"`
Columns []string `json:"columns"`
OmitColumns []string `json:"omit_columns"`
Filters []FilterOption `json:"filters"`
Limit *int `json:"limit"`
Offset *int `json:"offset"`
Updatable *bool `json:"updateable"` // if true, the relation can be updated
}
type FilterOption struct {
Column string `json:"column"`
Operator string `json:"operator"`
Value interface{} `json:"value"`
}
type SortOption struct {
Column string `json:"column"`
Direction string `json:"direction"`
}
type CustomOperator struct {
Name string `json:"name"`
SQL string `json:"sql"`
}
type ComputedColumn struct {
Name string `json:"name"`
Expression string `json:"expression"`
}
// Response structures
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Metadata *Metadata `json:"metadata,omitempty"`
Error *APIError `json:"error,omitempty"`
}
type Metadata struct {
Total int64 `json:"total"`
Filtered int64 `json:"filtered"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
Detail string `json:"detail,omitempty"`
}
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
IsNullable bool `json:"is_nullable"`
IsPrimary bool `json:"is_primary"`
IsUnique bool `json:"is_unique"`
HasIndex bool `json:"has_index"`
}
type TableMetadata struct {
Schema string `json:"schema"`
Table string `json:"table"`
Columns []Column `json:"columns"`
Relations []string `json:"relations"`
}

View File

@@ -1,78 +0,0 @@
package resolvespec
import (
"fmt"
"net/http"
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
"github.com/Warky-Devs/ResolveSpec/pkg/models"
"gorm.io/gorm"
)
func handleUpdateResult(w http.ResponseWriter, h *LegacyAPIHandler, result *gorm.DB, data interface{}) {
if result.Error != nil {
logger.Error("Update error: %v", result.Error)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", result.Error)
return
}
if result.RowsAffected == 0 {
logger.Warn("No records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
return
}
logger.Info("Successfully updated %d records", result.RowsAffected)
h.sendResponse(w, data, nil)
}
func optionalInt(ptr *int) int {
if ptr == nil {
return 0
}
return *ptr
}
// Helper methods
func (h *LegacyAPIHandler) applyFilter(query *gorm.DB, filter FilterOption) *gorm.DB {
switch filter.Operator {
case "eq":
return query.Where(fmt.Sprintf("%s = ?", filter.Column), filter.Value)
case "neq":
return query.Where(fmt.Sprintf("%s != ?", filter.Column), filter.Value)
case "gt":
return query.Where(fmt.Sprintf("%s > ?", filter.Column), filter.Value)
case "gte":
return query.Where(fmt.Sprintf("%s >= ?", filter.Column), filter.Value)
case "lt":
return query.Where(fmt.Sprintf("%s < ?", filter.Column), filter.Value)
case "lte":
return query.Where(fmt.Sprintf("%s <= ?", filter.Column), filter.Value)
case "like":
return query.Where(fmt.Sprintf("%s LIKE ?", filter.Column), filter.Value)
case "ilike":
return query.Where(fmt.Sprintf("%s ILIKE ?", filter.Column), filter.Value)
case "in":
return query.Where(fmt.Sprintf("%s IN (?)", filter.Column), filter.Value)
default:
return query
}
}
func (h *LegacyAPIHandler) getModelForEntity(schema, name string) (interface{}, error) {
model, err := models.GetModelByName(fmt.Sprintf("%s.%s", schema, name))
if err != nil {
model, err = models.GetModelByName(name)
}
return model, err
}
func (h *LegacyAPIHandler) RegisterModel(schema, name string, model interface{}) error {
fullname := fmt.Sprintf("%s.%s", schema, name)
oldModel, err := models.GetModelByName(fullname)
if oldModel != nil && err != nil {
return fmt.Errorf("model %s already exists", fullname)
}
err = models.RegisterModel(model, fullname)
return err
}