diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..46a7b00 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index be9e932..95723af 100644 --- a/README.md +++ b/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. + ![slogan](./generated_slogan.webp) +## 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 \ No newline at end of file +- AI used for documentation checking and correction +- Community feedback and contributions that made v2.0 possible \ No newline at end of file diff --git a/go.mod b/go.mod index 04443ca..6924157 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index f99265d..5c98243 100644 --- a/go.sum +++ b/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= diff --git a/pkg/resolvespec/apiHandler.go b/pkg/resolvespec/apiHandler.go index 73880e0..cfd3941 100644 --- a/pkg/resolvespec/apiHandler.go +++ b/pkg/resolvespec/apiHandler.go @@ -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{ diff --git a/pkg/resolvespec/bun_adapter.go b/pkg/resolvespec/bun_adapter.go new file mode 100644 index 0000000..7506626 --- /dev/null +++ b/pkg/resolvespec/bun_adapter.go @@ -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 +} \ No newline at end of file diff --git a/pkg/resolvespec/bunrouter_adapter.go b/pkg/resolvespec/bunrouter_adapter.go new file mode 100644 index 0000000..183c2d6 --- /dev/null +++ b/pkg/resolvespec/bunrouter_adapter.go @@ -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 + }) +} \ No newline at end of file diff --git a/pkg/resolvespec/compatibility.go b/pkg/resolvespec/compatibility.go new file mode 100644 index 0000000..0893282 --- /dev/null +++ b/pkg/resolvespec/compatibility.go @@ -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 +} \ No newline at end of file diff --git a/pkg/resolvespec/crud.go b/pkg/resolvespec/crud.go index b7f5a51..686f73a 100644 --- a/pkg/resolvespec/crud.go +++ b/pkg/resolvespec/crud.go @@ -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)) diff --git a/pkg/resolvespec/database.go b/pkg/resolvespec/database.go new file mode 100644 index 0000000..4f1525d --- /dev/null +++ b/pkg/resolvespec/database.go @@ -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 +} \ No newline at end of file diff --git a/pkg/resolvespec/gorm_adapter.go b/pkg/resolvespec/gorm_adapter.go new file mode 100644 index 0000000..643b2d1 --- /dev/null +++ b/pkg/resolvespec/gorm_adapter.go @@ -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 +} \ No newline at end of file diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go new file mode 100644 index 0000000..5d4fca3 --- /dev/null +++ b/pkg/resolvespec/handler.go @@ -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) +} \ No newline at end of file diff --git a/pkg/resolvespec/interfaces.go b/pkg/resolvespec/interfaces.go index 7641911..9e25c7f 100644 --- a/pkg/resolvespec/interfaces.go +++ b/pkg/resolvespec/interfaces.go @@ -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) diff --git a/pkg/resolvespec/meta.go b/pkg/resolvespec/meta.go index 6e62d7f..d8b5d70 100644 --- a/pkg/resolvespec/meta.go +++ b/pkg/resolvespec/meta.go @@ -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"] diff --git a/pkg/resolvespec/model_registry.go b/pkg/resolvespec/model_registry.go new file mode 100644 index 0000000..9731c88 --- /dev/null +++ b/pkg/resolvespec/model_registry.go @@ -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) +} \ No newline at end of file diff --git a/pkg/resolvespec/resolvespec.go b/pkg/resolvespec/resolvespec.go new file mode 100644 index 0000000..f660f72 --- /dev/null +++ b/pkg/resolvespec/resolvespec.go @@ -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()) +} \ No newline at end of file diff --git a/pkg/resolvespec/router_adapters.go b/pkg/resolvespec/router_adapters.go new file mode 100644 index 0000000..b66eb4e --- /dev/null +++ b/pkg/resolvespec/router_adapters.go @@ -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{}, + } +} \ No newline at end of file diff --git a/pkg/resolvespec/utils.go b/pkg/resolvespec/utils.go index f197285..11d2dee 100644 --- a/pkg/resolvespec/utils.go +++ b/pkg/resolvespec/utils.go @@ -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 {