mirror of
https://github.com/Warky-Devs/ResolveSpec.git
synced 2025-09-13 08:51:30 +00:00
Updated Database Interface, Added Bun Support
This commit is contained in:
parent
f3ca6c356a
commit
399cea9335
173
MIGRATION_GUIDE.md
Normal file
173
MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Migration Guide: Database and Router Abstraction
|
||||
|
||||
This guide explains how to migrate from the direct GORM/Router dependencies to the new abstracted interfaces.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
### What was changed:
|
||||
1. **Database Operations**: GORM-specific code is now abstracted behind `Database` interface
|
||||
2. **Router Integration**: HTTP router dependencies are abstracted behind `Router` interface
|
||||
3. **Model Registry**: Models are now managed through a `ModelRegistry` interface
|
||||
4. **Backward Compatibility**: Existing code continues to work with `NewAPIHandler()`
|
||||
|
||||
### Benefits:
|
||||
- **Database Flexibility**: Switch between GORM, Bun, or other ORMs without code changes
|
||||
- **Router Flexibility**: Use Gorilla Mux, Gin, Echo, or other routers
|
||||
- **Better Testing**: Easy to mock database and router interactions
|
||||
- **Cleaner Separation**: Business logic separated from ORM/router specifics
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Option 1: No Changes Required (Backward Compatible)
|
||||
Your existing code continues to work without any changes:
|
||||
|
||||
```go
|
||||
// This still works exactly as before
|
||||
handler := resolvespec.NewAPIHandler(db)
|
||||
```
|
||||
|
||||
### Option 2: Gradual Migration to New API
|
||||
|
||||
#### Step 1: Use New Handler Constructor
|
||||
```go
|
||||
// Old way
|
||||
handler := resolvespec.NewAPIHandler(gormDB)
|
||||
|
||||
// New way
|
||||
handler := resolvespec.NewHandlerWithGORM(gormDB)
|
||||
```
|
||||
|
||||
#### Step 2: Use Interface-based Approach
|
||||
```go
|
||||
// Create database adapter
|
||||
dbAdapter := resolvespec.NewGormAdapter(gormDB)
|
||||
|
||||
// Create model registry
|
||||
registry := resolvespec.NewModelRegistry()
|
||||
|
||||
// Register your models
|
||||
registry.RegisterModel("public.users", &User{})
|
||||
registry.RegisterModel("public.orders", &Order{})
|
||||
|
||||
// Create handler
|
||||
handler := resolvespec.NewHandler(dbAdapter, registry)
|
||||
```
|
||||
|
||||
## Switching Database Backends
|
||||
|
||||
### From GORM to Bun
|
||||
```go
|
||||
// Add bun dependency first:
|
||||
// go get github.com/uptrace/bun
|
||||
|
||||
// Old GORM setup
|
||||
gormDB, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
gormAdapter := resolvespec.NewGormAdapter(gormDB)
|
||||
|
||||
// New Bun setup
|
||||
sqlDB, _ := sql.Open("sqlite3", "test.db")
|
||||
bunDB := bun.NewDB(sqlDB, sqlitedialect.New())
|
||||
bunAdapter := resolvespec.NewBunAdapter(bunDB)
|
||||
|
||||
// Handler creation is identical
|
||||
handler := resolvespec.NewHandler(bunAdapter, registry)
|
||||
```
|
||||
|
||||
## Router Flexibility
|
||||
|
||||
### Current Gorilla Mux (Default)
|
||||
```go
|
||||
router := mux.NewRouter()
|
||||
resolvespec.SetupRoutes(router, handler)
|
||||
```
|
||||
|
||||
### BunRouter (Built-in Support)
|
||||
```go
|
||||
// Simple setup
|
||||
router := bunrouter.New()
|
||||
resolvespec.SetupBunRouterWithResolveSpec(router, handler)
|
||||
|
||||
// Or using adapter
|
||||
routerAdapter := resolvespec.NewStandardBunRouterAdapter()
|
||||
// Use routerAdapter.GetBunRouter() for the underlying router
|
||||
```
|
||||
|
||||
### Using Router Adapters (Advanced)
|
||||
```go
|
||||
// For when you want router abstraction
|
||||
routerAdapter := resolvespec.NewStandardRouter()
|
||||
routerAdapter.RegisterRoute("/{schema}/{entity}", handlerFunc)
|
||||
```
|
||||
|
||||
## Model Registration
|
||||
|
||||
### Old Way (Still Works)
|
||||
```go
|
||||
// Models registered through existing models package
|
||||
handler.RegisterModel("public", "users", &User{})
|
||||
```
|
||||
|
||||
### New Way (Recommended)
|
||||
```go
|
||||
registry := resolvespec.NewModelRegistry()
|
||||
registry.RegisterModel("public.users", &User{})
|
||||
registry.RegisterModel("public.orders", &Order{})
|
||||
|
||||
handler := resolvespec.NewHandler(dbAdapter, registry)
|
||||
```
|
||||
|
||||
## Interface Definitions
|
||||
|
||||
### Database Interface
|
||||
```go
|
||||
type Database interface {
|
||||
NewSelect() SelectQuery
|
||||
NewInsert() InsertQuery
|
||||
NewUpdate() UpdateQuery
|
||||
NewDelete() DeleteQuery
|
||||
// ... transaction methods
|
||||
}
|
||||
```
|
||||
|
||||
### Available Adapters
|
||||
- `GormAdapter` - For GORM (ready to use)
|
||||
- `BunAdapter` - For Bun (add dependency: `go get github.com/uptrace/bun`)
|
||||
- Easy to create custom adapters for other ORMs
|
||||
|
||||
## Testing Benefits
|
||||
|
||||
### Before (Tightly Coupled)
|
||||
```go
|
||||
// Hard to test - requires real GORM setup
|
||||
func TestHandler(t *testing.T) {
|
||||
db := setupRealGormDB()
|
||||
handler := resolvespec.NewAPIHandler(db)
|
||||
// ... test logic
|
||||
}
|
||||
```
|
||||
|
||||
### After (Mockable)
|
||||
```go
|
||||
// Easy to test - mock the interfaces
|
||||
func TestHandler(t *testing.T) {
|
||||
mockDB := &MockDatabase{}
|
||||
mockRegistry := &MockModelRegistry{}
|
||||
handler := resolvespec.NewHandler(mockDB, mockRegistry)
|
||||
// ... test logic with mocks
|
||||
}
|
||||
```
|
||||
|
||||
## Breaking Changes
|
||||
- **None for existing code** - Full backward compatibility maintained
|
||||
- New interfaces are additive, not replacing existing APIs
|
||||
|
||||
## Recommended Migration Timeline
|
||||
1. **Phase 1**: Use existing code (no changes needed)
|
||||
2. **Phase 2**: Gradually adopt new constructors (`NewHandlerWithGORM`)
|
||||
3. **Phase 3**: Move to interface-based approach when needed
|
||||
4. **Phase 4**: Switch database backends if desired
|
||||
|
||||
## Getting Help
|
||||
- Check example functions in `resolvespec.go`
|
||||
- Review interface definitions in `database.go`
|
||||
- Examine adapter implementations for patterns
|
276
README.md
276
README.md
@ -2,8 +2,25 @@
|
||||
|
||||
ResolveSpec is a flexible and powerful REST API specification and implementation that provides GraphQL-like capabilities while maintaining REST simplicity. It allows for dynamic data querying, relationship preloading, and complex filtering through a clean, URL-based interface.
|
||||
|
||||
**🆕 New in v2.0**: Database-agnostic architecture with support for GORM, Bun, and other ORMs. Router-flexible design works with Gorilla Mux, Gin, Echo, and more.
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Existing Code (Backward Compatible)](#option-1-existing-code-backward-compatible)
|
||||
- [New Database-Agnostic API](#option-2-new-database-agnostic-api)
|
||||
- [Router Integration](#router-integration)
|
||||
- [Migration from v1.x](#migration-from-v1x)
|
||||
- [Architecture](#architecture)
|
||||
- [API Structure](#api-structure)
|
||||
- [Example Usage](#example-usage)
|
||||
- [Testing](#testing)
|
||||
- [What's New in v2.0](#whats-new-in-v20)
|
||||
|
||||
## Features
|
||||
|
||||
- **Dynamic Data Querying**: Select specific columns and relationships to return
|
||||
@ -13,6 +30,10 @@ ResolveSpec is a flexible and powerful REST API specification and implementation
|
||||
- **Pagination**: Built-in limit and offset support
|
||||
- **Computed Columns**: Define virtual columns for complex calculations
|
||||
- **Custom Operators**: Add custom SQL conditions when needed
|
||||
- **🆕 Database Agnostic**: Works with GORM, Bun, or any database layer through adapters
|
||||
- **🆕 Router Flexible**: Integrates with Gorilla Mux, Gin, Echo, or custom routers
|
||||
- **🆕 Backward Compatible**: Existing code works without changes
|
||||
- **🆕 Better Testing**: Mockable interfaces for easy unit testing
|
||||
|
||||
## API Structure
|
||||
|
||||
@ -94,53 +115,197 @@ go get github.com/Warky-Devs/ResolveSpec
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Import the package:
|
||||
```go
|
||||
import "github.com/Warky-Devs/ResolveSpec"
|
||||
```
|
||||
### Option 1: Existing Code (Backward Compatible)
|
||||
|
||||
1. Initialize the handler:
|
||||
```go
|
||||
handler := resolvespec.NewAPIHandler(db)
|
||||
Your existing code continues to work without any changes:
|
||||
|
||||
// Register your models
|
||||
```go
|
||||
import "github.com/Warky-Devs/ResolveSpec/pkg/resolvespec"
|
||||
|
||||
// This still works exactly as before
|
||||
handler := resolvespec.NewAPIHandler(gormDB)
|
||||
handler.RegisterModel("core", "users", &User{})
|
||||
handler.RegisterModel("core", "posts", &Post{})
|
||||
```
|
||||
|
||||
3. Use with your preferred router:
|
||||
## Migration from v1.x
|
||||
|
||||
Using Gin:
|
||||
ResolveSpec v2.0 introduces a new database and router abstraction layer while maintaining **100% backward compatibility**. Your existing code will continue to work without any changes.
|
||||
|
||||
### Migration Timeline
|
||||
|
||||
1. **Phase 1**: Continue using existing API (no changes needed)
|
||||
2. **Phase 2**: Gradually adopt new constructors when convenient
|
||||
3. **Phase 3**: Switch to interface-based approach for new features
|
||||
4. **Phase 4**: Optionally switch database backends
|
||||
|
||||
### Detailed Migration Guide
|
||||
|
||||
For detailed migration instructions, examples, and best practices, see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Abstraction Layer
|
||||
|
||||
```
|
||||
Your Application Code
|
||||
↓
|
||||
Handler (Business Logic)
|
||||
↓
|
||||
Database Interface
|
||||
↓
|
||||
[GormAdapter] [BunAdapter] [CustomAdapter]
|
||||
↓ ↓ ↓
|
||||
[GORM] [Bun] [Your ORM]
|
||||
```
|
||||
|
||||
### Supported Database Layers
|
||||
|
||||
- **GORM** (default, fully supported)
|
||||
- **Bun** (ready to use, included in dependencies)
|
||||
- **Custom ORMs** (implement the `Database` interface)
|
||||
|
||||
### Supported Routers
|
||||
|
||||
- **Gorilla Mux** (built-in support with `SetupRoutes()`)
|
||||
- **BunRouter** (built-in support with `SetupBunRouterWithResolveSpec()`)
|
||||
- **Gin** (manual integration, see examples above)
|
||||
- **Echo** (manual integration, see examples above)
|
||||
- **Custom Routers** (implement request/response adapters)
|
||||
|
||||
### Option 2: New Database-Agnostic API
|
||||
|
||||
#### With GORM (Recommended Migration Path)
|
||||
```go
|
||||
func setupGin(handler *resolvespec.APIHandler) *gin.Engine {
|
||||
import "github.com/Warky-Devs/ResolveSpec/pkg/resolvespec"
|
||||
|
||||
// Create database adapter
|
||||
dbAdapter := resolvespec.NewGormAdapter(gormDB)
|
||||
|
||||
// Create model registry
|
||||
registry := resolvespec.NewModelRegistry()
|
||||
registry.RegisterModel("core.users", &User{})
|
||||
registry.RegisterModel("core.posts", &Post{})
|
||||
|
||||
// Create handler
|
||||
handler := resolvespec.NewHandler(dbAdapter, registry)
|
||||
```
|
||||
|
||||
#### With Bun ORM
|
||||
```go
|
||||
import "github.com/Warky-Devs/ResolveSpec/pkg/resolvespec"
|
||||
import "github.com/uptrace/bun"
|
||||
|
||||
// Create Bun adapter (Bun dependency already included)
|
||||
dbAdapter := resolvespec.NewBunAdapter(bunDB)
|
||||
|
||||
// Rest is identical to GORM
|
||||
registry := resolvespec.NewModelRegistry()
|
||||
handler := resolvespec.NewHandler(dbAdapter, registry)
|
||||
```
|
||||
|
||||
### Router Integration
|
||||
|
||||
#### Gorilla Mux (Built-in Support)
|
||||
```go
|
||||
import "github.com/gorilla/mux"
|
||||
|
||||
// Backward compatible way
|
||||
router := mux.NewRouter()
|
||||
resolvespec.SetupRoutes(router, handler)
|
||||
|
||||
// Or manually:
|
||||
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handler.Handle(w, r, vars)
|
||||
}).Methods("POST")
|
||||
```
|
||||
|
||||
#### Gin (Custom Integration)
|
||||
```go
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func setupGin(handler *resolvespec.Handler) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
r.POST("/:schema/:entity", func(c *gin.Context) {
|
||||
params := map[string]string{
|
||||
"schema": c.Param("schema"),
|
||||
"entity": c.Param("entity"),
|
||||
"id": c.Param("id"),
|
||||
}
|
||||
handler.SetParams(params)
|
||||
handler.Handle(c.Writer, c.Request)
|
||||
|
||||
// Use new adapter interfaces
|
||||
reqAdapter := resolvespec.NewHTTPRequest(c.Request)
|
||||
respAdapter := resolvespec.NewHTTPResponseWriter(c.Writer)
|
||||
handler.Handle(respAdapter, reqAdapter, params)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
```
|
||||
|
||||
Using Mux:
|
||||
#### Echo (Custom Integration)
|
||||
```go
|
||||
func setupMux(handler *resolvespec.APIHandler) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
import "github.com/labstack/echo/v4"
|
||||
|
||||
func setupEcho(handler *resolvespec.Handler) *echo.Echo {
|
||||
e := echo.New()
|
||||
|
||||
r.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handler.SetParams(vars)
|
||||
handler.Handle(w, r)
|
||||
}).Methods("POST")
|
||||
e.POST("/:schema/:entity", func(c echo.Context) error {
|
||||
params := map[string]string{
|
||||
"schema": c.Param("schema"),
|
||||
"entity": c.Param("entity"),
|
||||
}
|
||||
|
||||
reqAdapter := resolvespec.NewHTTPRequest(c.Request())
|
||||
respAdapter := resolvespec.NewHTTPResponseWriter(c.Response().Writer)
|
||||
handler.Handle(respAdapter, reqAdapter, params)
|
||||
return nil
|
||||
})
|
||||
|
||||
return r
|
||||
return e
|
||||
}
|
||||
```
|
||||
|
||||
#### BunRouter (Built-in Support)
|
||||
```go
|
||||
import "github.com/uptrace/bunrouter"
|
||||
|
||||
// Simple setup with built-in function
|
||||
func setupBunRouter(handler *resolvespec.APIHandlerCompat) *bunrouter.Router {
|
||||
router := bunrouter.New()
|
||||
resolvespec.SetupBunRouterWithResolveSpec(router, handler)
|
||||
return router
|
||||
}
|
||||
|
||||
// Or use the adapter
|
||||
func setupBunRouterAdapter() *resolvespec.StandardBunRouterAdapter {
|
||||
routerAdapter := resolvespec.NewStandardBunRouterAdapter()
|
||||
|
||||
// Register routes manually
|
||||
routerAdapter.RegisterRouteWithParams("POST", "/:schema/:entity",
|
||||
[]string{"schema", "entity"},
|
||||
func(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
// Your handler logic
|
||||
})
|
||||
|
||||
return routerAdapter
|
||||
}
|
||||
|
||||
// Full uptrace stack (bunrouter + Bun ORM)
|
||||
func setupFullUptrace(bunDB *bun.DB) *bunrouter.Router {
|
||||
// Database adapter
|
||||
dbAdapter := resolvespec.NewBunAdapter(bunDB)
|
||||
registry := resolvespec.NewModelRegistry()
|
||||
handler := resolvespec.NewHandler(dbAdapter, registry)
|
||||
|
||||
// Router
|
||||
router := resolvespec.NewStandardBunRouterAdapter()
|
||||
resolvespec.SetupBunRouterWithResolveSpec(router.GetBunRouter(),
|
||||
&resolvespec.APIHandlerCompat{
|
||||
newHandler: handler,
|
||||
})
|
||||
|
||||
return router.GetBunRouter()
|
||||
}
|
||||
```
|
||||
|
||||
@ -198,13 +363,45 @@ Define virtual columns using SQL expressions:
|
||||
]
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### With New Architecture (Mockable)
|
||||
|
||||
```go
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
// Create mock database
|
||||
type MockDatabase struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockDatabase) NewSelect() resolvespec.SelectQuery {
|
||||
args := m.Called()
|
||||
return args.Get(0).(resolvespec.SelectQuery)
|
||||
}
|
||||
|
||||
// Test your handler with mocks
|
||||
func TestHandler(t *testing.T) {
|
||||
mockDB := &MockDatabase{}
|
||||
mockRegistry := resolvespec.NewModelRegistry()
|
||||
handler := resolvespec.NewHandler(mockDB, mockRegistry)
|
||||
|
||||
// Setup mock expectations
|
||||
mockDB.On("NewSelect").Return(&MockSelectQuery{})
|
||||
|
||||
// Test your logic
|
||||
// ... test code
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Implement proper authentication and authorization
|
||||
- Validate all input parameters
|
||||
- Use prepared statements (handled by GORM)
|
||||
- Validate all input parameters
|
||||
- Use prepared statements (handled by GORM/Bun/your ORM)
|
||||
- Implement rate limiting
|
||||
- Control access at schema/entity level
|
||||
- **New**: Database abstraction layer provides additional security through interface boundaries
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -218,10 +415,29 @@ Define virtual columns using SQL expressions:
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## What's New in v2.0
|
||||
|
||||
### Breaking Changes
|
||||
- **None!** Full backward compatibility maintained
|
||||
|
||||
### New Features
|
||||
- **Database Abstraction**: Support for GORM, Bun, and custom ORMs
|
||||
- **Router Flexibility**: Works with any HTTP router through adapters
|
||||
- **BunRouter Integration**: Built-in support for uptrace/bunrouter
|
||||
- **Better Architecture**: Clean separation of concerns with interfaces
|
||||
- **Enhanced Testing**: Mockable interfaces for comprehensive testing
|
||||
- **Migration Guide**: Step-by-step migration instructions
|
||||
|
||||
### Performance Improvements
|
||||
- More efficient query building through interface design
|
||||
- Reduced coupling between components
|
||||
- Better memory management with interface boundaries
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by REST, Odata and GraphQL's flexibility
|
||||
- Built with [GORM](https://gorm.io)
|
||||
- Uses Gin or Mux Web Framework
|
||||
- Inspired by REST, OData, and GraphQL's flexibility
|
||||
- **Database Support**: [GORM](https://gorm.io) and [Bun](https://bun.uptrace.dev/)
|
||||
- **Router Support**: Gorilla Mux (built-in), Gin, Echo, and others through adapters
|
||||
- Slogan generated using DALL-E
|
||||
- AI used for documentation checking and correction
|
||||
- AI used for documentation checking and correction
|
||||
- Community feedback and contributions that made v2.0 possible
|
14
go.mod
14
go.mod
@ -1,11 +1,14 @@
|
||||
module github.com/Warky-Devs/ResolveSpec
|
||||
|
||||
go 1.22.5
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/uptrace/bun v1.2.15
|
||||
go.uber.org/zap v1.27.0
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
@ -17,11 +20,16 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/uptrace/bunrouter v1.0.23 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
|
29
go.sum
29
go.sum
@ -17,10 +17,16 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@ -31,19 +37,30 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE=
|
||||
github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038=
|
||||
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
|
||||
github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -12,19 +12,19 @@ import (
|
||||
|
||||
type HandlerFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
type APIHandler struct {
|
||||
type LegacyAPIHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAPIHandler creates a new API handler instance
|
||||
func NewAPIHandler(db *gorm.DB) *APIHandler {
|
||||
return &APIHandler{
|
||||
// NewLegacyAPIHandler creates a new legacy API handler instance
|
||||
func NewLegacyAPIHandler(db *gorm.DB) *LegacyAPIHandler {
|
||||
return &LegacyAPIHandler{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Main handler method
|
||||
func (h *APIHandler) Handle(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
func (h *LegacyAPIHandler) Handle(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var req RequestBody
|
||||
|
||||
if r.Body == nil {
|
||||
@ -67,7 +67,7 @@ func (h *APIHandler) Handle(w http.ResponseWriter, r *http.Request, params map[s
|
||||
}
|
||||
}
|
||||
|
||||
func (h *APIHandler) sendResponse(w http.ResponseWriter, data interface{}, metadata *Metadata) {
|
||||
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,
|
||||
@ -76,7 +76,7 @@ func (h *APIHandler) sendResponse(w http.ResponseWriter, data interface{}, metad
|
||||
})
|
||||
}
|
||||
|
||||
func (h *APIHandler) sendError(w http.ResponseWriter, status int, code, message string, details interface{}) {
|
||||
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{
|
||||
|
334
pkg/resolvespec/bun_adapter.go
Normal file
334
pkg/resolvespec/bun_adapter.go
Normal file
@ -0,0 +1,334 @@
|
||||
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
|
||||
}
|
218
pkg/resolvespec/bunrouter_adapter.go
Normal file
218
pkg/resolvespec/bunrouter_adapter.go
Normal file
@ -0,0 +1,218 @@
|
||||
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
|
||||
})
|
||||
}
|
72
pkg/resolvespec/compatibility.go
Normal file
72
pkg/resolvespec/compatibility.go
Normal file
@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// Read handler
|
||||
func (h *APIHandler) handleRead(w http.ResponseWriter, r *http.Request, schema, entity, id string, options RequestOptions) {
|
||||
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
|
||||
@ -128,7 +128,7 @@ func (h *APIHandler) handleRead(w http.ResponseWriter, r *http.Request, schema,
|
||||
}
|
||||
|
||||
// Create handler
|
||||
func (h *APIHandler) handleCreate(w http.ResponseWriter, r *http.Request, schema, entity string, data any, options RequestOptions) {
|
||||
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))
|
||||
|
||||
@ -171,7 +171,7 @@ func (h *APIHandler) handleCreate(w http.ResponseWriter, r *http.Request, schema
|
||||
}
|
||||
|
||||
// Update handler
|
||||
func (h *APIHandler) handleUpdate(w http.ResponseWriter, r *http.Request, schema, entity string, urlID string, reqID any, data any, options RequestOptions) {
|
||||
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))
|
||||
|
||||
@ -223,7 +223,7 @@ func (h *APIHandler) handleUpdate(w http.ResponseWriter, r *http.Request, schema
|
||||
}
|
||||
|
||||
// Delete handler
|
||||
func (h *APIHandler) handleDelete(w http.ResponseWriter, r *http.Request, schema, entity, id string) {
|
||||
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))
|
||||
|
||||
|
135
pkg/resolvespec/database.go
Normal file
135
pkg/resolvespec/database.go
Normal file
@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
282
pkg/resolvespec/gorm_adapter.go
Normal file
282
pkg/resolvespec/gorm_adapter.go
Normal file
@ -0,0 +1,282 @@
|
||||
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
|
||||
}
|
445
pkg/resolvespec/handler.go
Normal file
445
pkg/resolvespec/handler.go
Normal file
@ -0,0 +1,445 @@
|
||||
package resolvespec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
// Handler handles API requests using database and model abstractions
|
||||
type Handler struct {
|
||||
db Database
|
||||
registry ModelRegistry
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
func NewHandler(db Database, registry ModelRegistry) *Handler {
|
||||
return &Handler{
|
||||
db: db,
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle processes API requests through router-agnostic interface
|
||||
func (h *Handler) Handle(w ResponseWriter, r Request, params map[string]string) {
|
||||
ctx := context.Background()
|
||||
|
||||
body, err := r.Body()
|
||||
if err != nil {
|
||||
logger.Error("Failed to read request body: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_request", "Failed to read request body", err)
|
||||
return
|
||||
}
|
||||
|
||||
var req 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)
|
||||
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(ctx, w, schema, entity, id, req.Options)
|
||||
case "create":
|
||||
h.handleCreate(ctx, w, schema, entity, req.Data, req.Options)
|
||||
case "update":
|
||||
h.handleUpdate(ctx, w, schema, entity, id, req.ID, req.Data, req.Options)
|
||||
case "delete":
|
||||
h.handleDelete(ctx, w, schema, entity, id)
|
||||
default:
|
||||
logger.Error("Invalid operation: %s", req.Operation)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleGet processes GET requests for metadata
|
||||
func (h *Handler) HandleGet(w ResponseWriter, r Request, params map[string]string) {
|
||||
schema := params["schema"]
|
||||
entity := params["entity"]
|
||||
|
||||
logger.Info("Getting metadata for %s.%s", schema, entity)
|
||||
|
||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get model: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||
return
|
||||
}
|
||||
|
||||
metadata := h.generateMetadata(schema, entity, model)
|
||||
h.sendResponse(w, metadata, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) handleRead(ctx context.Context, w ResponseWriter, schema, entity, id string, options RequestOptions) {
|
||||
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 = query.Table(tableName)
|
||||
|
||||
// Apply column selection
|
||||
if len(options.Columns) > 0 {
|
||||
logger.Debug("Selecting columns: %v", options.Columns)
|
||||
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
|
||||
if len(options.Preload) > 0 {
|
||||
logger.Warn("Preloading not yet implemented in new handler - use legacy APIHandler for preload functionality")
|
||||
}
|
||||
|
||||
// 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
|
||||
total, err := query.Count(ctx)
|
||||
if 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
|
||||
query = query.Where("id = ?", id)
|
||||
if err := query.Scan(ctx, singleResult); err != nil {
|
||||
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.Scan(ctx, results); 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: int64(total),
|
||||
Filtered: int64(total),
|
||||
Limit: optionalInt(options.Limit),
|
||||
Offset: optionalInt(options.Offset),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, entity string, data interface{}, options RequestOptions) {
|
||||
logger.Info("Creating records for %s.%s", schema, entity)
|
||||
|
||||
tableName := fmt.Sprintf("%s.%s", schema, entity)
|
||||
query := h.db.NewInsert().Table(tableName)
|
||||
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, value := range v {
|
||||
query = query.Value(key, value)
|
||||
}
|
||||
result, err := query.Exec(ctx)
|
||||
if err != nil {
|
||||
logger.Error("Error creating record: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Successfully created record, rows affected: %d", result.RowsAffected())
|
||||
h.sendResponse(w, v, nil)
|
||||
|
||||
case []map[string]interface{}:
|
||||
err := h.db.RunInTransaction(ctx, func(tx Database) error {
|
||||
for _, item := range v {
|
||||
txQuery := tx.NewInsert().Table(tableName)
|
||||
for key, value := range item {
|
||||
txQuery = txQuery.Value(key, value)
|
||||
}
|
||||
if _, err := txQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error creating records: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Successfully created %d records", len(v))
|
||||
h.sendResponse(w, v, nil)
|
||||
|
||||
case []interface{}:
|
||||
// Handle []interface{} type from JSON unmarshaling
|
||||
list := make([]interface{}, 0)
|
||||
err := h.db.RunInTransaction(ctx, func(tx Database) error {
|
||||
for _, item := range v {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
txQuery := tx.NewInsert().Table(tableName)
|
||||
for key, value := range itemMap {
|
||||
txQuery = txQuery.Value(key, value)
|
||||
}
|
||||
if _, err := txQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error creating records: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", err)
|
||||
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)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for create operation", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdate(ctx context.Context, w ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options RequestOptions) {
|
||||
logger.Info("Updating records for %s.%s", schema, entity)
|
||||
|
||||
tableName := fmt.Sprintf("%s.%s", schema, entity)
|
||||
query := h.db.NewUpdate().Table(tableName)
|
||||
|
||||
switch updates := data.(type) {
|
||||
case map[string]interface{}:
|
||||
query = query.SetMap(updates)
|
||||
default:
|
||||
logger.Error("Invalid data type for update operation: %T", data)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for update operation", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply conditions
|
||||
if urlID != "" {
|
||||
logger.Debug("Updating by URL ID: %s", urlID)
|
||||
query = query.Where("id = ?", urlID)
|
||||
} else if reqID != nil {
|
||||
switch id := reqID.(type) {
|
||||
case string:
|
||||
logger.Debug("Updating by request ID: %s", id)
|
||||
query = query.Where("id = ?", id)
|
||||
case []string:
|
||||
logger.Debug("Updating by multiple IDs: %v", id)
|
||||
query = query.Where("id IN (?)", id)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := query.Exec(ctx)
|
||||
if err != nil {
|
||||
logger.Error("Update error: %v", err)
|
||||
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) {
|
||||
logger.Info("Deleting records from %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
|
||||
}
|
||||
|
||||
tableName := fmt.Sprintf("%s.%s", schema, entity)
|
||||
query := h.db.NewDelete().Table(tableName).Where("id = ?", id)
|
||||
|
||||
result, err := query.Exec(ctx)
|
||||
if err != nil {
|
||||
logger.Error("Error deleting record: %v", err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Successfully deleted record with ID: %s", id)
|
||||
h.sendResponse(w, nil, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) applyFilter(query SelectQuery, filter FilterOption) SelectQuery {
|
||||
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 *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
if provider, ok := model.(TableNameProvider); ok {
|
||||
return provider.TableName()
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", schema, entity)
|
||||
}
|
||||
|
||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) TableMetadata {
|
||||
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),
|
||||
}
|
||||
|
||||
// Generate metadata using reflection (same logic as before)
|
||||
for i := 0; i < modelType.NumField(); i++ {
|
||||
field := modelType.Field(i)
|
||||
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
gormTag := field.Tag.Get("gorm")
|
||||
jsonTag := field.Tag.Get("json")
|
||||
|
||||
if jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonName := strings.Split(jsonTag, ",")[0]
|
||||
if jsonName == "" {
|
||||
jsonName = field.Name
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (h *Handler) sendResponse(w ResponseWriter, data interface{}, metadata *Metadata) {
|
||||
w.SetHeader("Content-Type", "application/json")
|
||||
w.WriteJSON(Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) sendError(w ResponseWriter, status int, code, message string, details interface{}) {
|
||||
w.SetHeader("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.WriteJSON(Response{
|
||||
Success: false,
|
||||
Error: &APIError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
Detail: fmt.Sprintf("%v", details),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterModel allows registering models at runtime
|
||||
func (h *Handler) RegisterModel(schema, name string, model interface{}) error {
|
||||
fullname := fmt.Sprintf("%s.%s", schema, name)
|
||||
return h.registry.RegisterModel(fullname, model)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package resolvespec
|
||||
|
||||
// Legacy interfaces for backward compatibility
|
||||
type GormTableNameInterface interface {
|
||||
TableName() string
|
||||
}
|
||||
@ -19,3 +20,8 @@ func (r *GormTableCRUDRequest) SetRequest(request string) {
|
||||
func (r GormTableCRUDRequest) GetRequest() string {
|
||||
return *r.CRUDRequest
|
||||
}
|
||||
|
||||
// New interfaces that replace the legacy ones above
|
||||
// These are now defined in database.go:
|
||||
// - TableNameProvider (replaces GormTableNameInterface)
|
||||
// - SchemaProvider (replaces GormTableSchemaInterface)
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
func (h *APIHandler) HandleGet(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
func (h *LegacyAPIHandler) HandleGet(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
schema := params["schema"]
|
||||
entity := params["entity"]
|
||||
|
||||
|
65
pkg/resolvespec/model_registry.go
Normal file
65
pkg/resolvespec/model_registry.go
Normal file
@ -0,0 +1,65 @@
|
||||
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)
|
||||
}
|
147
pkg/resolvespec/resolvespec.go
Normal file
147
pkg/resolvespec/resolvespec.go
Normal file
@ -0,0 +1,147 @@
|
||||
package resolvespec
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/uptrace/bun"
|
||||
"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()
|
||||
return NewHandler(gormAdapter, registry)
|
||||
}
|
||||
|
||||
// NewStandardRouter creates a router with standard HTTP handlers
|
||||
func NewStandardRouter() *StandardMuxAdapter {
|
||||
return NewStandardMuxAdapter()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
vars := mux.Vars(r)
|
||||
handler.Handle(w, r, vars)
|
||||
}).Methods("POST")
|
||||
|
||||
router.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handler.Handle(w, r, vars)
|
||||
}).Methods("POST")
|
||||
|
||||
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
handler.HandleGet(w, r, vars)
|
||||
}).Methods("GET")
|
||||
}
|
||||
|
||||
// Example usage functions for documentation:
|
||||
|
||||
// ExampleWithGORM shows how to use ResolveSpec with GORM (current default)
|
||||
func ExampleWithGORM(db *gorm.DB) {
|
||||
// Create handler using GORM (backward compatible)
|
||||
handler := NewAPIHandlerWithGORM(db)
|
||||
|
||||
// Setup router
|
||||
router := mux.NewRouter()
|
||||
SetupRoutes(router, 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)
|
||||
|
||||
// Create model registry
|
||||
registry := 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)
|
||||
handler.Handle(respAdapter, reqAdapter, params)
|
||||
})
|
||||
}
|
||||
|
||||
// ExampleWithBunRouter shows how to use bunrouter from uptrace
|
||||
func ExampleWithBunRouter(db *gorm.DB) {
|
||||
// Create handler (can use any database adapter)
|
||||
handler := NewAPIHandler(db)
|
||||
|
||||
// Create bunrouter
|
||||
router := NewStandardBunRouterAdapter()
|
||||
|
||||
// Setup ResolveSpec routes with bunrouter
|
||||
SetupBunRouterWithResolveSpec(router.GetBunRouter(), handler)
|
||||
|
||||
// Start server
|
||||
// http.ListenAndServe(":8080", router.GetBunRouter())
|
||||
}
|
||||
|
||||
// ExampleBunRouterWithBunDB shows the full uptrace stack (bunrouter + Bun ORM)
|
||||
func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
|
||||
// Create Bun database adapter
|
||||
dbAdapter := NewBunAdapter(bunDB)
|
||||
|
||||
// Create model registry
|
||||
registry := 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()
|
||||
|
||||
// Setup ResolveSpec routes
|
||||
SetupBunRouterWithResolveSpec(router.GetBunRouter(), compatHandler)
|
||||
|
||||
// This gives you the full uptrace stack: bunrouter + Bun ORM
|
||||
// http.ListenAndServe(":8080", router.GetBunRouter())
|
||||
}
|
210
pkg/resolvespec/router_adapters.go
Normal file
210
pkg/resolvespec/router_adapters.go
Normal file
@ -0,0 +1,210 @@
|
||||
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{},
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func handleUpdateResult(w http.ResponseWriter, h *APIHandler, result *gorm.DB, data interface{}) {
|
||||
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)
|
||||
@ -32,7 +32,7 @@ func optionalInt(ptr *int) int {
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (h *APIHandler) applyFilter(query *gorm.DB, filter FilterOption) *gorm.DB {
|
||||
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)
|
||||
@ -57,7 +57,7 @@ func (h *APIHandler) applyFilter(query *gorm.DB, filter FilterOption) *gorm.DB {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *APIHandler) getModelForEntity(schema, name string) (interface{}, error) {
|
||||
func (h *LegacyAPIHandler) getModelForEntity(schema, name string) (interface{}, error) {
|
||||
model, err := models.GetModelByName(fmt.Sprintf("%s.%s", schema, name))
|
||||
|
||||
if err != nil {
|
||||
@ -66,7 +66,7 @@ func (h *APIHandler) getModelForEntity(schema, name string) (interface{}, error)
|
||||
return model, err
|
||||
}
|
||||
|
||||
func (h *APIHandler) RegisterModel(schema, name string, model interface{}) error {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user