mirror of
https://github.com/Warky-Devs/ResolveSpec.git
synced 2025-05-18 20:57:29 +00:00
Initial Spec done. More work to do. Need to bring in Argitek designs
This commit is contained in:
parent
7ce04e2032
commit
f284e55a5c
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
bin/
|
27
.vscode/launch.json
vendored
Normal file
27
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"cwd": "${workspaceFolder}/bin",
|
||||||
|
"program": "${workspaceFolder}/cmd/testserver/main.go",
|
||||||
|
"args": [
|
||||||
|
"-o","${workspaceFolder}/bin/debug_bin"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"args": [
|
||||||
|
"-o","${workspaceFolder}/bin/debug_bin.exe"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"CGO_ENABLED": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "go",
|
||||||
|
"label": "go: build workspace",
|
||||||
|
"command": "build",
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"CGO_ENABLED": "0"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}/bin",
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"../..."
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
"$go"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "go",
|
||||||
|
"label": "go: test workspace",
|
||||||
|
"command": "test",
|
||||||
|
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"CGO_ENABLED": "0"
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}/bin",
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"../..."
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
"$go"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
229
README.md
229
README.md
@ -1,2 +1,227 @@
|
|||||||
# ResolveSpec
|
# 📜 ResolveSpec 📜
|
||||||
ResolveSpec📜 is a flexible and powerful REST API specification and implementation that provides GraphQL-like capabilities while maintaining REST simplicity
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Dynamic Data Querying**: Select specific columns and relationships to return
|
||||||
|
- **Relationship Preloading**: Load related entities with custom column selection and filters
|
||||||
|
- **Complex Filtering**: Apply multiple filters with various operators
|
||||||
|
- **Sorting**: Multi-column sort support
|
||||||
|
- **Pagination**: Built-in limit and offset support
|
||||||
|
- **Computed Columns**: Define virtual columns for complex calculations
|
||||||
|
- **Custom Operators**: Add custom SQL conditions when needed
|
||||||
|
|
||||||
|
## API Structure
|
||||||
|
|
||||||
|
### URL Patterns
|
||||||
|
```
|
||||||
|
/[schema]/[table_or_entity]/[id]
|
||||||
|
/[schema]/[table_or_entity]
|
||||||
|
/[schema]/[function]
|
||||||
|
/[schema]/[virtual]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operation": "read|create|update|delete",
|
||||||
|
"data": {
|
||||||
|
// For create/update operations
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"preload": [...],
|
||||||
|
"columns": [...],
|
||||||
|
"filters": [...],
|
||||||
|
"sort": [...],
|
||||||
|
"limit": number,
|
||||||
|
"offset": number,
|
||||||
|
"customOperators": [...],
|
||||||
|
"computedColumns": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Reading Data with Related Entities
|
||||||
|
```json
|
||||||
|
POST /core/users
|
||||||
|
{
|
||||||
|
"operation": "read",
|
||||||
|
"options": {
|
||||||
|
"columns": ["id", "name", "email"],
|
||||||
|
"preload": [
|
||||||
|
{
|
||||||
|
"relation": "posts",
|
||||||
|
"columns": ["id", "title"],
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"column": "status",
|
||||||
|
"operator": "eq",
|
||||||
|
"value": "published"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"column": "active",
|
||||||
|
"operator": "eq",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort": [
|
||||||
|
{
|
||||||
|
"column": "created_at",
|
||||||
|
"direction": "desc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limit": 10,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/Warky-Devs/ResolveSpec
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Import the package:
|
||||||
|
```go
|
||||||
|
import "github.com/Warky-Devs/ResolveSpec"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Initialize the handler:
|
||||||
|
```go
|
||||||
|
handler := resolvespec.NewAPIHandler(db)
|
||||||
|
|
||||||
|
// Register your models
|
||||||
|
handler.RegisterModel("core", "users", &User{})
|
||||||
|
handler.RegisterModel("core", "posts", &Post{})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use with your preferred router:
|
||||||
|
|
||||||
|
Using Gin:
|
||||||
|
```go
|
||||||
|
func setupGin(handler *resolvespec.APIHandler) *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)
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using Mux:
|
||||||
|
```go
|
||||||
|
func setupMux(handler *resolvespec.APIHandler) *mux.Router {
|
||||||
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
r.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.SetParams(vars)
|
||||||
|
handler.Handle(w, r)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Model Registration
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.RegisterModel("core", "users", &User{})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features in Detail
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
Supported operators:
|
||||||
|
- eq: Equal
|
||||||
|
- neq: Not Equal
|
||||||
|
- gt: Greater Than
|
||||||
|
- gte: Greater Than or Equal
|
||||||
|
- lt: Less Than
|
||||||
|
- lte: Less Than or Equal
|
||||||
|
- like: LIKE pattern matching
|
||||||
|
- ilike: Case-insensitive LIKE
|
||||||
|
- in: IN clause
|
||||||
|
|
||||||
|
### Sorting
|
||||||
|
Support for multiple sort criteria with direction:
|
||||||
|
```json
|
||||||
|
"sort": [
|
||||||
|
{
|
||||||
|
"column": "created_at",
|
||||||
|
"direction": "desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column": "name",
|
||||||
|
"direction": "asc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Computed Columns
|
||||||
|
Define virtual columns using SQL expressions:
|
||||||
|
```json
|
||||||
|
"computedColumns": [
|
||||||
|
{
|
||||||
|
"name": "full_name",
|
||||||
|
"expression": "CONCAT(first_name, ' ', last_name)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Implement proper authentication and authorization
|
||||||
|
- Validate all input parameters
|
||||||
|
- Use prepared statements (handled by GORM)
|
||||||
|
- Implement rate limiting
|
||||||
|
- Control access at schema/entity level
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Inspired by REST, Odata and GraphQL's flexibility
|
||||||
|
- Built with [GORM](https://gorm.io)
|
||||||
|
- Uses Gin or Mux Web Framework
|
||||||
|
- Slogan generated using DALL-E
|
||||||
|
- AI used for documentation checking and correction
|
1
bin/.readme
Normal file
1
bin/.readme
Normal file
@ -0,0 +1 @@
|
|||||||
|
Binary compiled files and working dir
|
95
cmd/testserver/main.go
Normal file
95
cmd/testserver/main.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/models"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/testmodels"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/resolvespec"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gormlog "gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize logger
|
||||||
|
fmt.Println("ResolveSpec test server starting")
|
||||||
|
logger.Init(true)
|
||||||
|
|
||||||
|
// Init Models
|
||||||
|
testmodels.RegisterTestModels()
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
db, err := initDB()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to initialize database: %+v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
// Initialize API handler
|
||||||
|
handler := resolvespec.NewAPIHandler(db)
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
r.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.Handle(w, r, vars)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
r.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.Handle(w, r, vars)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
r.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.HandleGet(w, r, vars)
|
||||||
|
}).Methods("GET")
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
logger.Info("Starting server on :8080")
|
||||||
|
if err := http.ListenAndServe(":8080", r); err != nil {
|
||||||
|
logger.Error("Server failed to start: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB() (*gorm.DB, error) {
|
||||||
|
|
||||||
|
newLogger := gormlog.New(
|
||||||
|
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
|
||||||
|
gormlog.Config{
|
||||||
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
|
LogLevel: gormlog.Info, // Log level
|
||||||
|
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||||
|
ParameterizedQueries: true, // Don't include params in the SQL log
|
||||||
|
Colorful: true, // Disable color
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create SQLite database
|
||||||
|
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{Logger: newLogger, FullSaveAssociations: false})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
modelList := models.GetModels()
|
||||||
|
|
||||||
|
// Auto migrate schemas
|
||||||
|
err = db.AutoMigrate(modelList...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
BIN
generated_slogan.webp
Normal file
BIN
generated_slogan.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 352 KiB |
31
go.mod
Normal file
31
go.mod
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module github.com/Warky-Devs/ResolveSpec
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.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
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
|
)
|
59
go.sum
Normal file
59
go.sum
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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=
|
||||||
|
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/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/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=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
|
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
1
internal/.readme
Normal file
1
internal/.readme
Normal file
@ -0,0 +1 @@
|
|||||||
|
Internal utils and files. Non exported
|
362
openapi.yaml
Normal file
362
openapi.yaml
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: ResolveSpec API
|
||||||
|
version: '1.0'
|
||||||
|
description: A flexible REST API with GraphQL-like capabilities
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: 'http://api.example.com/v1'
|
||||||
|
|
||||||
|
paths:
|
||||||
|
'/{schema}/{entity}':
|
||||||
|
parameters:
|
||||||
|
- name: schema
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: entity
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
summary: Get table metadata
|
||||||
|
description: Retrieve table metadata including columns, types, and relationships
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/Response'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/TableMetadata'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/ServerError'
|
||||||
|
post:
|
||||||
|
summary: Perform operations on entities
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Request'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Response'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/ServerError'
|
||||||
|
|
||||||
|
'/{schema}/{entity}/{id}':
|
||||||
|
parameters:
|
||||||
|
- name: schema
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: entity
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
post:
|
||||||
|
summary: Perform operations on a specific entity
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Request'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Response'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/ServerError'
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Request:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- operation
|
||||||
|
properties:
|
||||||
|
operation:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- read
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- delete
|
||||||
|
id:
|
||||||
|
oneOf:
|
||||||
|
- type: string
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Optional record identifier(s) when not provided in URL
|
||||||
|
data:
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
description: Data for single or bulk create/update operations
|
||||||
|
options:
|
||||||
|
$ref: '#/components/schemas/Options'
|
||||||
|
|
||||||
|
Options:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
preload:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PreloadOption'
|
||||||
|
columns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
filters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/FilterOption'
|
||||||
|
sort:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SortOption'
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
offset:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
customOperators:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomOperator'
|
||||||
|
computedColumns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ComputedColumn'
|
||||||
|
|
||||||
|
PreloadOption:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
relation:
|
||||||
|
type: string
|
||||||
|
columns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
filters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/FilterOption'
|
||||||
|
|
||||||
|
FilterOption:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- column
|
||||||
|
- operator
|
||||||
|
- value
|
||||||
|
properties:
|
||||||
|
column:
|
||||||
|
type: string
|
||||||
|
operator:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- eq
|
||||||
|
- neq
|
||||||
|
- gt
|
||||||
|
- gte
|
||||||
|
- lt
|
||||||
|
- lte
|
||||||
|
- like
|
||||||
|
- ilike
|
||||||
|
- in
|
||||||
|
value:
|
||||||
|
type: object
|
||||||
|
|
||||||
|
SortOption:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- column
|
||||||
|
- direction
|
||||||
|
properties:
|
||||||
|
column:
|
||||||
|
type: string
|
||||||
|
direction:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- asc
|
||||||
|
- desc
|
||||||
|
|
||||||
|
CustomOperator:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- sql
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
sql:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
ComputedColumn:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- expression
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
expression:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Response:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
metadata:
|
||||||
|
$ref: '#/components/schemas/Metadata'
|
||||||
|
error:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
Metadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
filtered:
|
||||||
|
type: integer
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
offset:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
details:
|
||||||
|
type: object
|
||||||
|
|
||||||
|
TableMetadata:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- schema
|
||||||
|
- table
|
||||||
|
- columns
|
||||||
|
- relations
|
||||||
|
properties:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Schema name
|
||||||
|
table:
|
||||||
|
type: string
|
||||||
|
description: Table name
|
||||||
|
columns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Column'
|
||||||
|
relations:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: List of relation names
|
||||||
|
|
||||||
|
Column:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- is_nullable
|
||||||
|
- is_primary
|
||||||
|
- is_unique
|
||||||
|
- has_index
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Column name
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Data type of the column
|
||||||
|
is_nullable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the column can contain null values
|
||||||
|
is_primary:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the column is a primary key
|
||||||
|
is_unique:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the column has a unique constraint
|
||||||
|
has_index:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the column is indexed
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Bad request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Response'
|
||||||
|
|
||||||
|
NotFound:
|
||||||
|
description: Resource not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Response'
|
||||||
|
|
||||||
|
ServerError:
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Response'
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
72
pkg/logger/logger.go
Normal file
72
pkg/logger/logger.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Logger *zap.SugaredLogger
|
||||||
|
|
||||||
|
func Init(dev bool) {
|
||||||
|
|
||||||
|
if dev {
|
||||||
|
cfg := zap.NewDevelopmentConfig()
|
||||||
|
UpdateLogger(&cfg)
|
||||||
|
} else {
|
||||||
|
cfg := zap.NewProductionConfig()
|
||||||
|
UpdateLogger(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLogger(config *zap.Config) {
|
||||||
|
defaultConfig := zap.NewProductionConfig()
|
||||||
|
defaultConfig.OutputPaths = []string{"resolvespec.log"}
|
||||||
|
if config == nil {
|
||||||
|
config = &defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := config.Build()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger = logger.Sugar()
|
||||||
|
Info("ResolveSpec Logger initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(template string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
log.Printf(template, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Logger.Infow(fmt.Sprintf(template, args...), "process_id", os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(template string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
log.Printf(template, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Logger.Warnw(fmt.Sprintf(template, args...), "process_id", os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(template string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
log.Printf(template, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Logger.Errorw(fmt.Sprintf(template, args...), "process_id", os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(template string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
log.Printf(template, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Logger.Debugw(fmt.Sprintf(template, args...), "process_id", os.Getpid())
|
||||||
|
}
|
70
pkg/models/registry.go
Normal file
70
pkg/models/registry.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
modelRegistry = make(map[string]interface{})
|
||||||
|
functionRegistry = make(map[string]interface{})
|
||||||
|
modelRegistryMutex sync.RWMutex
|
||||||
|
funcRegistryMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterModel registers a model type with the registry
|
||||||
|
// The model must be a struct or a pointer to a struct
|
||||||
|
// e.g RegisterModel(&ModelPublicUser{},"public.user")
|
||||||
|
func RegisterModel(model interface{}, name string) {
|
||||||
|
modelRegistryMutex.Lock()
|
||||||
|
defer modelRegistryMutex.Unlock()
|
||||||
|
|
||||||
|
modelType := reflect.TypeOf(model)
|
||||||
|
if modelType.Kind() == reflect.Ptr {
|
||||||
|
modelType = modelType.Elem()
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = modelType.Name()
|
||||||
|
}
|
||||||
|
modelRegistry[name] = model
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFunction register a function with the registry
|
||||||
|
func RegisterFunction(fn interface{}, name string) {
|
||||||
|
funcRegistryMutex.Lock()
|
||||||
|
defer funcRegistryMutex.Unlock()
|
||||||
|
functionRegistry[name] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelByName retrieves a model from the registry by its type name
|
||||||
|
func GetModelByName(name string) (interface{}, error) {
|
||||||
|
modelRegistryMutex.RLock()
|
||||||
|
defer modelRegistryMutex.RUnlock()
|
||||||
|
|
||||||
|
if modelRegistry[name] == nil {
|
||||||
|
return nil, fmt.Errorf("model not found: %s", name)
|
||||||
|
}
|
||||||
|
return modelRegistry[name], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateModels iterates over all models in the registry
|
||||||
|
func IterateModels(fn func(name string, model interface{})) {
|
||||||
|
modelRegistryMutex.RLock()
|
||||||
|
defer modelRegistryMutex.RUnlock()
|
||||||
|
|
||||||
|
for name, model := range modelRegistry {
|
||||||
|
fn(name, model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModels returns a list of all models in the registry
|
||||||
|
func GetModels() []interface{} {
|
||||||
|
models := make([]interface{}, 0)
|
||||||
|
modelRegistryMutex.RLock()
|
||||||
|
defer modelRegistryMutex.RUnlock()
|
||||||
|
for _, model := range modelRegistry {
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
}
|
76
pkg/resolvespec/apiHandler.go
Normal file
76
pkg/resolvespec/apiHandler.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerFunc func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIHandler creates a new API handler instance
|
||||||
|
func NewAPIHandler(db *gorm.DB) *APIHandler {
|
||||||
|
return &APIHandler{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main handler method
|
||||||
|
func (h *APIHandler) Handle(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
var req RequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
logger.Error("Failed to decode request body: %v", err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := params["schema"]
|
||||||
|
entity := params["entity"]
|
||||||
|
id := params["id"]
|
||||||
|
|
||||||
|
logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity)
|
||||||
|
|
||||||
|
switch req.Operation {
|
||||||
|
case "read":
|
||||||
|
h.handleRead(w, r, schema, entity, id, req.Options)
|
||||||
|
case "create":
|
||||||
|
h.handleCreate(w, r, schema, entity, req.Data, req.Options)
|
||||||
|
case "update":
|
||||||
|
h.handleUpdate(w, r, schema, entity, id, req.ID, req.Data, req.Options)
|
||||||
|
case "delete":
|
||||||
|
h.handleDelete(w, r, schema, entity, id)
|
||||||
|
default:
|
||||||
|
logger.Error("Invalid operation: %s", req.Operation)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APIHandler) sendResponse(w http.ResponseWriter, data interface{}, metadata *Metadata) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(Response{
|
||||||
|
Success: true,
|
||||||
|
Data: data,
|
||||||
|
Metadata: metadata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APIHandler) sendError(w http.ResponseWriter, status int, code, message string, details interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(Response{
|
||||||
|
Success: false,
|
||||||
|
Error: &APIError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Details: details,
|
||||||
|
Detail: fmt.Sprintf("%v", details),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
250
pkg/resolvespec/crud.go
Normal file
250
pkg/resolvespec/crud.go
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read handler
|
||||||
|
func (h *APIHandler) handleRead(w http.ResponseWriter, r *http.Request, schema, entity, id string, options RequestOptions) {
|
||||||
|
logger.Info("Reading records from %s.%s", schema, entity)
|
||||||
|
|
||||||
|
// Get the model struct for the entity
|
||||||
|
model, err := h.getModelForEntity(schema, entity)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Invalid entity: %v", err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
GormTableNameInterface, ok := model.(GormTableNameInterface)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("Model does not implement GormTableNameInterface")
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "model_error", "Model does not implement GormTableNameInterface", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := h.db.Model(model).Table(GormTableNameInterface.TableName())
|
||||||
|
|
||||||
|
// Apply column selection
|
||||||
|
if len(options.Columns) > 0 {
|
||||||
|
logger.Debug("Selecting columns: %v", options.Columns)
|
||||||
|
query = query.Select(options.Columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply preloading
|
||||||
|
for _, preload := range options.Preload {
|
||||||
|
logger.Debug("Applying preload for relation: %s", preload.Relation)
|
||||||
|
query = query.Preload(preload.Relation, func(db *gorm.DB) *gorm.DB {
|
||||||
|
|
||||||
|
if len(preload.Columns) > 0 {
|
||||||
|
db = db.Select(preload.Columns)
|
||||||
|
}
|
||||||
|
if len(preload.Filters) > 0 {
|
||||||
|
for _, filter := range preload.Filters {
|
||||||
|
db = h.applyFilter(db, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
for _, filter := range options.Filters {
|
||||||
|
logger.Debug("Applying filter: %s %s %v", filter.Column, filter.Operator, filter.Value)
|
||||||
|
query = h.applyFilter(query, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
for _, sort := range options.Sort {
|
||||||
|
direction := "ASC"
|
||||||
|
if strings.ToLower(sort.Direction) == "desc" {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
logger.Debug("Applying sort: %s %s", sort.Column, direction)
|
||||||
|
query = query.Order(fmt.Sprintf("%s %s", sort.Column, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count before pagination
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
logger.Error("Error counting records: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "query_error", "Error counting records", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug("Total records before filtering: %d", total)
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
if options.Limit != nil && *options.Limit > 0 {
|
||||||
|
logger.Debug("Applying limit: %d", *options.Limit)
|
||||||
|
query = query.Limit(*options.Limit)
|
||||||
|
}
|
||||||
|
if options.Offset != nil && *options.Offset > 0 {
|
||||||
|
logger.Debug("Applying offset: %d", *options.Offset)
|
||||||
|
query = query.Offset(*options.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query
|
||||||
|
var result interface{}
|
||||||
|
if id != "" {
|
||||||
|
logger.Debug("Querying single record with ID: %s", id)
|
||||||
|
singleResult := model
|
||||||
|
if err := query.First(singleResult, id).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
logger.Warn("Record not found with ID: %s", id)
|
||||||
|
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("Error querying record: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result = singleResult
|
||||||
|
} else {
|
||||||
|
logger.Debug("Querying multiple records")
|
||||||
|
sliceType := reflect.SliceOf(reflect.TypeOf(model))
|
||||||
|
results := reflect.New(sliceType).Interface()
|
||||||
|
|
||||||
|
if err := query.Find(results).Error; err != nil {
|
||||||
|
logger.Error("Error querying records: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result = reflect.ValueOf(results).Elem().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Successfully retrieved records")
|
||||||
|
h.sendResponse(w, result, &Metadata{
|
||||||
|
Total: total,
|
||||||
|
Filtered: total,
|
||||||
|
Limit: optionalInt(options.Limit),
|
||||||
|
Offset: optionalInt(options.Offset),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handler
|
||||||
|
func (h *APIHandler) handleCreate(w http.ResponseWriter, r *http.Request, schema, entity string, data any, options RequestOptions) {
|
||||||
|
logger.Info("Creating records for %s.%s", schema, entity)
|
||||||
|
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
|
||||||
|
|
||||||
|
switch v := data.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
result := query.Create(v)
|
||||||
|
if result.Error != nil {
|
||||||
|
logger.Error("Error creating record: %v", result.Error)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", result.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Successfully created record")
|
||||||
|
h.sendResponse(w, v, nil)
|
||||||
|
|
||||||
|
case []map[string]interface{}:
|
||||||
|
result := query.Create(v)
|
||||||
|
if result.Error != nil {
|
||||||
|
logger.Error("Error creating records: %v", result.Error)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", result.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Successfully created %d records", len(v))
|
||||||
|
h.sendResponse(w, v, nil)
|
||||||
|
case []interface{}:
|
||||||
|
list := make([]interface{}, 0)
|
||||||
|
for _, item := range v {
|
||||||
|
result := query.Create(item)
|
||||||
|
list = append(list, item)
|
||||||
|
if result.Error != nil {
|
||||||
|
logger.Error("Error creating records: %v", result.Error)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", result.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Successfully created %d records", len(v))
|
||||||
|
}
|
||||||
|
h.sendResponse(w, list, nil)
|
||||||
|
default:
|
||||||
|
logger.Error("Invalid data type for create operation: %T", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handler
|
||||||
|
func (h *APIHandler) handleUpdate(w http.ResponseWriter, r *http.Request, schema, entity string, urlID string, reqID any, data any, options RequestOptions) {
|
||||||
|
logger.Info("Updating records for %s.%s", schema, entity)
|
||||||
|
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case urlID != "":
|
||||||
|
logger.Debug("Updating by URL ID: %s", urlID)
|
||||||
|
result := query.Where("id = ?", urlID).Updates(data)
|
||||||
|
handleUpdateResult(w, h, result, data)
|
||||||
|
|
||||||
|
case reqID != nil:
|
||||||
|
switch id := reqID.(type) {
|
||||||
|
case string:
|
||||||
|
logger.Debug("Updating by request ID: %s", id)
|
||||||
|
result := query.Where("id = ?", id).Updates(data)
|
||||||
|
handleUpdateResult(w, h, result, data)
|
||||||
|
|
||||||
|
case []string:
|
||||||
|
logger.Debug("Updating by multiple IDs: %v", id)
|
||||||
|
result := query.Where("id IN ?", id).Updates(data)
|
||||||
|
handleUpdateResult(w, h, result, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case data != nil:
|
||||||
|
switch v := data.(type) {
|
||||||
|
case []map[string]interface{}:
|
||||||
|
logger.Debug("Performing bulk update with %d records", len(v))
|
||||||
|
err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, item := range v {
|
||||||
|
if id, ok := item["id"].(string); ok {
|
||||||
|
if err := tx.Where("id = ?", id).Updates(item).Error; err != nil {
|
||||||
|
logger.Error("Error in bulk update transaction: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "update_error", "Error in bulk update", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Bulk update completed successfully")
|
||||||
|
h.sendResponse(w, data, nil)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.Error("Invalid data type for update operation: %T", data)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete handler
|
||||||
|
func (h *APIHandler) handleDelete(w http.ResponseWriter, r *http.Request, schema, entity, id string) {
|
||||||
|
logger.Info("Deleting records from %s.%s", schema, entity)
|
||||||
|
query := h.db.Table(fmt.Sprintf("%s.%s", schema, entity))
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
logger.Error("Delete operation requires an ID")
|
||||||
|
h.sendError(w, http.StatusBadRequest, "missing_id", "Delete operation requires an ID", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := query.Delete("id = ?", id)
|
||||||
|
if result.Error != nil {
|
||||||
|
logger.Error("Error deleting record: %v", result.Error)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting record", result.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.Warn("No record found to delete with ID: %s", id)
|
||||||
|
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Successfully deleted record with ID: %s", id)
|
||||||
|
h.sendResponse(w, nil, nil)
|
||||||
|
}
|
9
pkg/resolvespec/interfaces.go
Normal file
9
pkg/resolvespec/interfaces.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
type GormTableNameInterface interface {
|
||||||
|
TableName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GormTableSchemaInterface interface {
|
||||||
|
TableSchema() string
|
||||||
|
}
|
131
pkg/resolvespec/meta.go
Normal file
131
pkg/resolvespec/meta.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *APIHandler) HandleGet(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||||
|
schema := params["schema"]
|
||||||
|
entity := params["entity"]
|
||||||
|
|
||||||
|
logger.Info("Getting metadata for %s.%s", schema, entity)
|
||||||
|
|
||||||
|
// Get model for the entity
|
||||||
|
model, err := h.getModelForEntity(schema, entity)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to get model: %v", err)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modelType := reflect.TypeOf(model)
|
||||||
|
if modelType.Kind() == reflect.Ptr {
|
||||||
|
modelType = modelType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := TableMetadata{
|
||||||
|
Schema: schema,
|
||||||
|
Table: entity,
|
||||||
|
Columns: make([]Column, 0),
|
||||||
|
Relations: make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get field information using reflection
|
||||||
|
for i := 0; i < modelType.NumField(); i++ {
|
||||||
|
field := modelType.Field(i)
|
||||||
|
|
||||||
|
// Skip unexported fields
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse GORM tags
|
||||||
|
gormTag := field.Tag.Get("gorm")
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
|
||||||
|
// Skip if json tag is "-"
|
||||||
|
if jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get JSON field name
|
||||||
|
jsonName := strings.Split(jsonTag, ",")[0]
|
||||||
|
if jsonName == "" {
|
||||||
|
jsonName = field.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a relation
|
||||||
|
if field.Type.Kind() == reflect.Slice ||
|
||||||
|
(field.Type.Kind() == reflect.Struct && field.Type.Name() != "Time") {
|
||||||
|
metadata.Relations = append(metadata.Relations, jsonName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
column := Column{
|
||||||
|
Name: jsonName,
|
||||||
|
Type: getColumnType(field),
|
||||||
|
IsNullable: isNullable(field),
|
||||||
|
IsPrimary: strings.Contains(gormTag, "primaryKey"),
|
||||||
|
IsUnique: strings.Contains(gormTag, "unique") || strings.Contains(gormTag, "uniqueIndex"),
|
||||||
|
HasIndex: strings.Contains(gormTag, "index") || strings.Contains(gormTag, "uniqueIndex"),
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Columns = append(metadata.Columns, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.sendResponse(w, metadata, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColumnType(field reflect.StructField) string {
|
||||||
|
// Check GORM type tag first
|
||||||
|
gormTag := field.Tag.Get("gorm")
|
||||||
|
if strings.Contains(gormTag, "type:") {
|
||||||
|
parts := strings.Split(gormTag, "type:")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
typePart := strings.Split(parts[1], ";")[0]
|
||||||
|
return typePart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Go types to SQL types
|
||||||
|
switch field.Type.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return "string"
|
||||||
|
case reflect.Int, reflect.Int32:
|
||||||
|
return "integer"
|
||||||
|
case reflect.Int64:
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Float32:
|
||||||
|
return "float"
|
||||||
|
case reflect.Float64:
|
||||||
|
return "double"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
default:
|
||||||
|
if field.Type.Name() == "Time" {
|
||||||
|
return "timestamp"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNullable(field reflect.StructField) bool {
|
||||||
|
// Check if it's a pointer type
|
||||||
|
if field.Type.Kind() == reflect.Ptr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a null type from sql package
|
||||||
|
typeName := field.Type.Name()
|
||||||
|
if strings.HasPrefix(typeName, "Null") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check GORM tags
|
||||||
|
gormTag := field.Tag.Get("gorm")
|
||||||
|
return !strings.Contains(gormTag, "not null")
|
||||||
|
}
|
95
pkg/resolvespec/types.go
Normal file
95
pkg/resolvespec/types.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
type RequestBody struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
ID *int64 `json:"id"`
|
||||||
|
Options RequestOptions `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOptions struct {
|
||||||
|
Preload []PreloadOption `json:"preload"`
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
OmitColumns []string `json:"omit_columns"`
|
||||||
|
Filters []FilterOption `json:"filters"`
|
||||||
|
Sort []SortOption `json:"sort"`
|
||||||
|
Limit *int `json:"limit"`
|
||||||
|
Offset *int `json:"offset"`
|
||||||
|
CustomOperators []CustomOperator `json:"customOperators"`
|
||||||
|
ComputedColumns []ComputedColumn `json:"computedColumns"`
|
||||||
|
Parameters []Parameter `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parameter struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Sequence *int `json:"sequence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreloadOption struct {
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
OmitColumns []string `json:"omit_columns"`
|
||||||
|
Filters []FilterOption `json:"filters"`
|
||||||
|
Limit *int `json:"limit"`
|
||||||
|
Offset *int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterOption struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Operator string `json:"operator"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortOption struct {
|
||||||
|
Column string `json:"column"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomOperator struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SQL string `json:"sql"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComputedColumn struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Expression string `json:"expression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structures
|
||||||
|
type Response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Metadata *Metadata `json:"metadata,omitempty"`
|
||||||
|
Error *APIError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Filtered int64 `json:"filtered"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details interface{} `json:"details,omitempty"`
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IsNullable bool `json:"is_nullable"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
IsUnique bool `json:"is_unique"`
|
||||||
|
HasIndex bool `json:"has_index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableMetadata struct {
|
||||||
|
Schema string `json:"schema"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
Columns []Column `json:"columns"`
|
||||||
|
Relations []string `json:"relations"`
|
||||||
|
}
|
67
pkg/resolvespec/utils.go
Normal file
67
pkg/resolvespec/utils.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package resolvespec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleUpdateResult(w http.ResponseWriter, h *APIHandler, result *gorm.DB, data interface{}) {
|
||||||
|
if result.Error != nil {
|
||||||
|
logger.Error("Update error: %v", result.Error)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", result.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.Warn("No records found to update")
|
||||||
|
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Successfully updated %d records", result.RowsAffected)
|
||||||
|
h.sendResponse(w, data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalInt(ptr *int) int {
|
||||||
|
if ptr == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
func (h *APIHandler) applyFilter(query *gorm.DB, filter FilterOption) *gorm.DB {
|
||||||
|
switch filter.Operator {
|
||||||
|
case "eq":
|
||||||
|
return query.Where(fmt.Sprintf("%s = ?", filter.Column), filter.Value)
|
||||||
|
case "neq":
|
||||||
|
return query.Where(fmt.Sprintf("%s != ?", filter.Column), filter.Value)
|
||||||
|
case "gt":
|
||||||
|
return query.Where(fmt.Sprintf("%s > ?", filter.Column), filter.Value)
|
||||||
|
case "gte":
|
||||||
|
return query.Where(fmt.Sprintf("%s >= ?", filter.Column), filter.Value)
|
||||||
|
case "lt":
|
||||||
|
return query.Where(fmt.Sprintf("%s < ?", filter.Column), filter.Value)
|
||||||
|
case "lte":
|
||||||
|
return query.Where(fmt.Sprintf("%s <= ?", filter.Column), filter.Value)
|
||||||
|
case "like":
|
||||||
|
return query.Where(fmt.Sprintf("%s LIKE ?", filter.Column), filter.Value)
|
||||||
|
case "ilike":
|
||||||
|
return query.Where(fmt.Sprintf("%s ILIKE ?", filter.Column), filter.Value)
|
||||||
|
case "in":
|
||||||
|
return query.Where(fmt.Sprintf("%s IN (?)", filter.Column), filter.Value)
|
||||||
|
default:
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APIHandler) getModelForEntity(schema, name string) (interface{}, error) {
|
||||||
|
model, err := models.GetModelByName(fmt.Sprintf("%s.%s", schema, name))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
model, err = models.GetModelByName(name)
|
||||||
|
}
|
||||||
|
return model, err
|
||||||
|
}
|
148
pkg/testmodels/business.go
Normal file
148
pkg/testmodels/business.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package testmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Department represents a company department
|
||||||
|
type Department struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code" gorm:"uniqueIndex"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Employees []Employee `json:"employees,omitempty" gorm:"foreignKey:DepartmentID;references:ID"`
|
||||||
|
Projects []Project `json:"projects,omitempty" gorm:"many2many:department_projects;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Department) TableName() string {
|
||||||
|
return "departments"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee represents a company employee
|
||||||
|
type Employee struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
Email string `json:"email" gorm:"uniqueIndex"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
DepartmentID string `json:"department_id" gorm:"type:string"`
|
||||||
|
ManagerID *string `json:"manager_id" gorm:"type:string"`
|
||||||
|
HireDate time.Time `json:"hire_date"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Department *Department `json:"department,omitempty" gorm:"foreignKey:DepartmentID;references:ID"`
|
||||||
|
Manager *Employee `json:"manager,omitempty" gorm:"foreignKey:ManagerID;references:ID"`
|
||||||
|
Reports []Employee `json:"reports,omitempty" gorm:"foreignKey:ManagerID;references:ID"`
|
||||||
|
Projects []Project `json:"projects,omitempty" gorm:"many2many:employee_projects;"`
|
||||||
|
Documents []Document `json:"documents,omitempty" gorm:"foreignKey:OwnerID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Employee) TableName() string {
|
||||||
|
return "employees"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project represents a company project
|
||||||
|
type Project struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code" gorm:"uniqueIndex"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartDate time.Time `json:"start_date"`
|
||||||
|
EndDate time.Time `json:"end_date"`
|
||||||
|
Budget float64 `json:"budget"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Departments []Department `json:"departments,omitempty" gorm:"many2many:department_projects;"`
|
||||||
|
Employees []Employee `json:"employees,omitempty" gorm:"many2many:employee_projects;"`
|
||||||
|
Tasks []ProjectTask `json:"tasks,omitempty" gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
Documents []Document `json:"documents,omitempty" gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Project) TableName() string {
|
||||||
|
return "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectTask represents a task within a project
|
||||||
|
type ProjectTask struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
ProjectID string `json:"project_id" gorm:"type:string"`
|
||||||
|
AssigneeID string `json:"assignee_id" gorm:"type:string"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
DueDate time.Time `json:"due_date"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
Assignee Employee `json:"assignee,omitempty" gorm:"foreignKey:AssigneeID;references:ID"`
|
||||||
|
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:TaskID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ProjectTask) TableName() string {
|
||||||
|
return "project_tasks"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document represents any document in the system
|
||||||
|
type Document struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
OwnerID string `json:"owner_id" gorm:"type:string"`
|
||||||
|
ProjectID *string `json:"project_id" gorm:"type:string"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Owner Employee `json:"owner,omitempty" gorm:"foreignKey:OwnerID;references:ID"`
|
||||||
|
Project *Project `json:"project,omitempty" gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Document) TableName() string {
|
||||||
|
return "documents"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment represents a comment on a task
|
||||||
|
type Comment struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;type:string"`
|
||||||
|
TaskID string `json:"task_id" gorm:"type:string"`
|
||||||
|
AuthorID string `json:"author_id" gorm:"type:string"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Task ProjectTask `json:"task,omitempty" gorm:"foreignKey:TaskID;references:ID"`
|
||||||
|
Author Employee `json:"author,omitempty" gorm:"foreignKey:AuthorID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Comment) TableName() string {
|
||||||
|
return "comments"
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterTestModels() {
|
||||||
|
models.RegisterModel(&Department{}, "departments")
|
||||||
|
models.RegisterModel(&Employee{}, "employees")
|
||||||
|
models.RegisterModel(&Project{}, "projects")
|
||||||
|
models.RegisterModel(&ProjectTask{}, "project_tasks")
|
||||||
|
models.RegisterModel(&Document{}, "documents")
|
||||||
|
models.RegisterModel(&Comment{}, "comments")
|
||||||
|
}
|
232
tests/integration_test.go
Normal file
232
tests/integration_test.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMain sets up the test environment
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
TestSetup(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDepartmentEmployees(t *testing.T) {
|
||||||
|
// Create test department
|
||||||
|
deptPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"id": "dept1",
|
||||||
|
"name": "Engineering",
|
||||||
|
"code": "ENG",
|
||||||
|
"description": "Engineering Department",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := makeRequest(t, "/test/departments", deptPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Create employees in department
|
||||||
|
empPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"id": "emp1",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"department_id": "dept1",
|
||||||
|
"title": "Senior Engineer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emp2",
|
||||||
|
"first_name": "Jane",
|
||||||
|
"last_name": "Smith",
|
||||||
|
"email": "jane@example.com",
|
||||||
|
"department_id": "dept1",
|
||||||
|
"title": "Engineer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/employees", empPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Read department with employees
|
||||||
|
readPayload := map[string]interface{}{
|
||||||
|
"operation": "read",
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"preload": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"relation": "employees",
|
||||||
|
"columns": []string{"id", "first_name", "last_name", "title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/departments/dept1", readPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
data := result["data"].(map[string]interface{})
|
||||||
|
employees := data["employees"].([]interface{})
|
||||||
|
assert.Equal(t, 2, len(employees))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmployeeHierarchy(t *testing.T) {
|
||||||
|
// Create manager
|
||||||
|
mgrPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"id": "mgr1",
|
||||||
|
"first_name": "Alice",
|
||||||
|
"last_name": "Manager",
|
||||||
|
"email": "alice@example.com",
|
||||||
|
"title": "Engineering Manager",
|
||||||
|
"department_id": "dept1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := makeRequest(t, "/test/employees", mgrPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Update employees to set manager
|
||||||
|
updatePayload := map[string]interface{}{
|
||||||
|
"operation": "update",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"manager_id": "mgr1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/employees/emp1", updatePayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
resp = makeRequest(t, "/test/employees/emp2", updatePayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Read manager with reports
|
||||||
|
readPayload := map[string]interface{}{
|
||||||
|
"operation": "read",
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"preload": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"relation": "reports",
|
||||||
|
"columns": []string{"id", "first_name", "last_name", "title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/employees/mgr1", readPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
data := result["data"].(map[string]interface{})
|
||||||
|
reports := data["reports"].([]interface{})
|
||||||
|
assert.Equal(t, 2, len(reports))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectStructure(t *testing.T) {
|
||||||
|
// Create project
|
||||||
|
projectPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"id": "proj1",
|
||||||
|
"name": "New Website",
|
||||||
|
"code": "WEB",
|
||||||
|
"description": "Company website redesign",
|
||||||
|
"status": "active",
|
||||||
|
"start_date": time.Now().Format(time.RFC3339),
|
||||||
|
"end_date": time.Now().AddDate(0, 3, 0).Format(time.RFC3339),
|
||||||
|
"budget": 100000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := makeRequest(t, "/test/projects", projectPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Create project tasks
|
||||||
|
taskPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"id": "task1",
|
||||||
|
"project_id": "proj1",
|
||||||
|
"assignee_id": "emp1",
|
||||||
|
"title": "Design Homepage",
|
||||||
|
"description": "Create homepage design",
|
||||||
|
"status": "in_progress",
|
||||||
|
"priority": 1,
|
||||||
|
"due_date": time.Now().AddDate(0, 1, 0).Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task2",
|
||||||
|
"project_id": "proj1",
|
||||||
|
"assignee_id": "emp2",
|
||||||
|
"title": "Implement Backend",
|
||||||
|
"description": "Implement backend APIs",
|
||||||
|
"status": "planned",
|
||||||
|
"priority": 2,
|
||||||
|
"due_date": time.Now().AddDate(0, 2, 0).Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/project_tasks", taskPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Create task comments
|
||||||
|
commentPayload := map[string]interface{}{
|
||||||
|
"operation": "create",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"id": "comment1",
|
||||||
|
"task_id": "task1",
|
||||||
|
"author_id": "mgr1",
|
||||||
|
"content": "Looking good! Please add more animations.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/comments", commentPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// Read project with all relations
|
||||||
|
readPayload := map[string]interface{}{
|
||||||
|
"operation": "read",
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"preload": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"relation": "tasks",
|
||||||
|
"columns": []string{"id", "title", "status", "assignee_id"},
|
||||||
|
"preload": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"relation": "comments",
|
||||||
|
"columns": []string{"id", "content", "author_id"},
|
||||||
|
"preload": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"relation": "author",
|
||||||
|
"columns": []string{"id", "first_name", "last_name"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "assignee",
|
||||||
|
"columns": []string{"id", "first_name", "last_name", "title"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = makeRequest(t, "/test/projects/proj1", readPayload)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
assert.True(t, result["success"].(bool))
|
||||||
|
}
|
152
tests/test_helpers.go
Normal file
152
tests/test_helpers.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/models"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/resolvespec"
|
||||||
|
"github.com/Warky-Devs/ResolveSpec/pkg/testmodels"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testDB *gorm.DB
|
||||||
|
testServer *httptest.Server
|
||||||
|
testServerURL string
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeRequest is a helper function to make HTTP requests in tests
|
||||||
|
func makeRequest(t *testing.T, path string, payload interface{}) *http.Response {
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
assert.NoError(t, err, "Failed to marshal request payload")
|
||||||
|
|
||||||
|
logger.Debug("Making request to %s with payload: %s", path, string(jsonData))
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", testServerURL+path, bytes.NewBuffer(jsonData))
|
||||||
|
assert.NoError(t, err, "Failed to create request")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "Failed to execute request")
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
logger.Error("Request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
} else {
|
||||||
|
logger.Debug("Request successful with status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyResponse is a helper function to verify response status and decode body
|
||||||
|
func verifyResponse(t *testing.T, resp *http.Response) map[string]interface{} {
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Unexpected response status")
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
assert.NoError(t, err, "Failed to decode response")
|
||||||
|
assert.True(t, result["success"].(bool), "Response indicates failure")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetup initializes the test environment
|
||||||
|
func TestSetup(m *testing.M) int {
|
||||||
|
logger.Init(true)
|
||||||
|
|
||||||
|
logger.Info("Setting up test environment")
|
||||||
|
|
||||||
|
// Create test database
|
||||||
|
db, err := setupTestDB()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to setup test database: %v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
testDB = db
|
||||||
|
|
||||||
|
// Setup test server
|
||||||
|
router := setupTestRouter(testDB)
|
||||||
|
testServer = httptest.NewServer(router)
|
||||||
|
|
||||||
|
fmt.Printf("ResolveSpec test server starting on %s\n", testServer.URL)
|
||||||
|
testServerURL = testServer.URL
|
||||||
|
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
logger.Info("Cleaning up test environment")
|
||||||
|
cleanup()
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupTestDB creates and initializes the test database
|
||||||
|
func setupTestDB() (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Models
|
||||||
|
testmodels.RegisterTestModels()
|
||||||
|
|
||||||
|
// Auto migrate all test models
|
||||||
|
err = autoMigrateModels(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to migrate models: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupTestRouter creates and configures the test router
|
||||||
|
func setupTestRouter(db *gorm.DB) http.Handler {
|
||||||
|
r := mux.NewRouter()
|
||||||
|
handler := resolvespec.NewAPIHandler(db)
|
||||||
|
|
||||||
|
r.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.Handle(w, r, vars)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
r.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
handler.Handle(w, r, vars)
|
||||||
|
}).Methods("POST")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup performs test cleanup
|
||||||
|
func cleanup() {
|
||||||
|
if testDB != nil {
|
||||||
|
db, err := testDB.DB()
|
||||||
|
if err == nil {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Remove("test.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoMigrateModels performs automigration for all test models
|
||||||
|
func autoMigrateModels(db *gorm.DB) error {
|
||||||
|
modelList := models.GetModels()
|
||||||
|
return db.AutoMigrate(modelList...)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user