Skip to content

Commit

Permalink
xgor
Browse files Browse the repository at this point in the history
  • Loading branch information
Ismael GraHms committed Dec 11, 2023
1 parent 49bb7b7 commit 1a3c725
Show file tree
Hide file tree
Showing 6 changed files with 624 additions and 8 deletions.
171 changes: 171 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# xgor

`xgor` is a library that extends [Gorm](https://gorm.io/) to provide additional functionalities for building robust database repositories with support for custom filters, transactions, and relationship handling.

## Features

- **Generic Repository:** Use generic repository patterns to handle common CRUD operations for your Gorm models.

- **Custom Filters:** Easily filter entities based on custom conditions using a flexible and intuitive filter syntax.

- **Transaction Support:** Perform operations within a transaction to ensure consistency and atomicity.

- **Relationship Handling:** Simplify relationship management with built-in functions for clearing relationships.

## Installation

```bash
go get -u github.com/grahms/xgor
```

## Usage

### Initializing a Repository

```go
import (
"gorm.io/gorm"
"github.com/grahms/xgor"
)

// Initialize DB
db, err := xgor.Open(...)

// Create a new repository
repo := xgor.New[BlogPost](db, errors.New("blog post not found"))

// Or with relationships
repoWithRelations := xgor.NewWithRelationships[BlogPost](db, errors.New("blog post not found"), "comments", "author")
```

### Adding a Blog Post

```go
post := &BlogPost{
Title: "Introduction to xgor",
Content: "Learn how to use xgor to supercharge your Gorm-based repositories.",
AuthorID: 1,
CategoryID: 2,
PublishedAt: time.Now(),
}
err := repo.Add(post)
```

### Querying Blog Posts with Custom Filters

```go
// Get all published posts in the "Technology" category written by a specific author
filters := xgor.FilterType{
"published_at__lte": time.Now(),
"category.name__eq": "Technology",
"author.id__eq": 1,
}
posts, err := repo.GetAll(nil, nil, nil, filters)
```

### Performing a Transaction

```go
err := repo.PerformTransaction(func(tx *gorm.DB) error {
// Update the author's profile and add a new blog post within the same transaction
author, err := authorRepo.GetByID(1)
if err != nil {
return err
}

author.Name = "Updated Author Name"
if err := authorRepo.Update(author); err != nil {
return err
}

newPost := &BlogPost{
Title: "Advanced xgor Techniques",
Content: "Explore advanced techniques for optimizing database queries with xgor.",
AuthorID: 1,
CategoryID: 3,
PublishedAt: time.Now(),
}

return repo.Add(newPost)
})
```

## Example Use Case: Blogging Application

Let's consider a blogging application where `xgor` is used to manage blog posts. In this scenario, `xgor` simplifies the data access layer, allowing developers to focus on building features rather than dealing with intricate database operations.

### Use Case Scenario

- **Scenario:** The application needs to fetch all published blog posts in a specific category written by a particular author.

- **Solution:** Utilize `xgor`'s custom filters to easily query the database and retrieve the required blog posts without the complexity of crafting intricate SQL queries.

```go
// Example: Get all published posts in the "Technology" category written by a specific author
filters := xgor.FilterType{
"published_at__lte": time.Now(),
"category.name__eq": "Technology",
"author.id__eq": 1,
}
posts, err := repo.GetAll(nil, nil, nil, filters)
```
## Example Use Case: Blogging Application (Pagination)

### Use Case Scenario

- **Scenario:** The blogging application needs to display a paginated list of blog posts on the homepage.

- **Solution:** Utilize `xgor` to implement pagination and retrieve a subset of blog posts for display.

```go
// Example: Get paginated blog posts for the homepage
limit := 10 // Number of posts per page
page := 1 // Current page
orderBy := "published_at desc" // Order posts by published date in descending order

// Use xgor to get paginated blog posts
paginationFilters := xgor.FilterType{"category_id__eq": 1} // Filter by category ID, if needed
blogPosts, err := repo.GetAll(&limit, &page, &orderBy, paginationFilters)

// Check for errors and handle the paginated blog posts
if err != nil {
// Handle error
} else {
// Access paginated results
totalPosts := blogPosts.TotalCount
currentPage := page
postsPerPage := limit
resultCount := blogPosts.ResultCount
displayedPosts := *blogPosts.Items

// Process and display paginated blog posts
for _, post := range displayedPosts {
// Process each blog post
}
}
```
## Custom Filters

Custom filters allow you to specify conditions for filtering entities. The filter syntax is based on the column name and a suffix that represents the condition. Here is a table of available filters:

| Filter | Description | Example |
|------------------|----------------------------------------------|-----------------------------------|
| `__eq` | Equals | `"age__eq": 25` |
| `__gt` | Greater Than | `"age__gt": 21` |
| `__lt` | Less Than | `"age__lt": 30` |
| `__gte` | Greater Than or Equal To | `"age__gte": 21` |
| `__lte` | Less Than or Equal To | `"age__lte": 30` |
| `__in` | In Array | `"age__in": []int{25, 30}` |
| `__not` | Not Equal To | `"age__not": 25` |
| `__not_in` | Not In Array | `"age__not_in": []int{25, 30}` |
| `__like` | Like (substring match) | `"name__like": "John"` |

Combine these filters to create powerful and flexible queries tailored to your application's needs.

## Contributing

Feel free to contribute by opening issues or submitting pull requests. Please follow the [Contributing Guidelines](CONTRIBUTING.md).

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
67 changes: 67 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"errors"
"fmt"
"github.com/grahms/xgor"
"gorm.io/driver/sqlite"
)

// Product Define a sample entity
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Price float64
}

func main() {
// Connect to an in-memory SQLite database
db, err := xgor.Open(sqlite.Open("file::memory:"))
if err != nil {
fmt.Println("Error connecting to the database:", err)
return
}

// Migrate the database schema
err = db.AutoMigrate(&Product{})
if err != nil {
fmt.Println("Error migrating database schema:", err)
return
}

// Create a repository for the Product entity
productRepo := xgor.New[Product](db, errors.New("product not found"))

// Add a product
newProduct := &Product{Name: "Laptop", Price: 999.99}
err = productRepo.Add(newProduct)
if err != nil {
fmt.Println("Error adding product:", err)
return
}

// Update the product
updatedProduct := &Product{ID: newProduct.ID, Name: "Updated Laptop", Price: 1099.99}
err = productRepo.Update(updatedProduct)
if err != nil {
fmt.Println("Error updating product:", err)
return
}

// Retrieve a product by ID
retrievedProduct, err := productRepo.GetByID(newProduct.ID)
if err != nil {
fmt.Println("Error getting product by ID:", err)
return
}
fmt.Println("Retrieved Product:", retrievedProduct)

// Use custom filters to get products with a specific condition
filters := xgor.FilterType{"price__gt": 1000.0}
highPricedProducts, err := productRepo.GetAll(nil, nil, nil, filters)
if err != nil {
fmt.Println("Error getting high-priced products:", err)
return
}
fmt.Println("High-Priced Products:", highPricedProducts.Items)
}
12 changes: 10 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
module Rxgor
module github.com/grahms/xgor

go 1.20

require gorm.io/gorm v1.25.5
require (
github.com/stretchr/testify v1.8.4
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
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/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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
16 changes: 10 additions & 6 deletions exgor.go → xgor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strings"
)

type filterType map[string]any
type FilterType map[string]any
type ListItems[T any] struct {
Items *[]T
TotalCount int64
Expand All @@ -18,8 +18,8 @@ type Repository[T any] interface {
Update(item *T) error
Delete(item *T) error
GetByID(id interface{}) (*T, error)
GetWithCustomFilters(filters filterType) (*T, error)
GetAll(limit *int, offset *int, orderBy *string, filters filterType) (ListItems[T], error)
GetWithCustomFilters(filters FilterType) (*T, error)
GetAll(limit *int, offset *int, orderBy *string, filters FilterType) (ListItems[T], error)
PerformTransaction(fn func(tx *gorm.DB) error) error
DeleteRelationship(item *T, relationship string) error
}
Expand Down Expand Up @@ -66,7 +66,7 @@ func (r *BaseRepository[T]) Delete(item *T) error {
return r.db.Delete(item).Error
}

func (r *BaseRepository[T]) GetWithCustomFilters(filters filterType) (*T, error) {
func (r *BaseRepository[T]) GetWithCustomFilters(filters FilterType) (*T, error) {
limit := 1
entity, err := r.GetAll(&limit, nil, nil, filters)
if err != nil || entity.TotalCount == 0 {
Expand All @@ -88,7 +88,7 @@ func (r *BaseRepository[T]) GetByID(id interface{}) (*T, error) {
return entity, nil
}

func (r *BaseRepository[T]) GetAll(limit *int, offset *int, orderBy *string, filters filterType) (ListItems[T], error) {
func (r *BaseRepository[T]) GetAll(limit *int, offset *int, orderBy *string, filters FilterType) (ListItems[T], error) {
var total int64

countQuery := r.preload().Model(new(T)).Scopes(r.applyFilters(filters))
Expand Down Expand Up @@ -144,7 +144,7 @@ func (r *BaseRepository[T]) DeleteRelationship(item *T, relationship string) err
return r.db.Model(item).Association(relationship).Clear()
}

func (r *BaseRepository[T]) applyFilters(filters filterType) func(db *gorm.DB) *gorm.DB {
func (r *BaseRepository[T]) applyFilters(filters FilterType) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
for c, v := range filters {
col, val := c, v
Expand Down Expand Up @@ -201,3 +201,7 @@ func (r *BaseRepository[T]) orderBy(orderBy *string) func(db *gorm.DB) *gorm.DB
return db
}
}

func Open(dialector gorm.Dialector, opts ...gorm.Option) (*gorm.DB, error) {
return gorm.Open(dialector, opts...)
}
Loading

0 comments on commit 1a3c725

Please sign in to comment.