Skip to content

Commit

Permalink
merge upstream to add UT.
Browse files Browse the repository at this point in the history
* merge

* fix UT to my format

* fix UT panic when setting a header

* fix UT

Co-authored-by: magellan <[email protected]>
  • Loading branch information
magellancl and magellan authored Dec 9, 2022
1 parent 9f0ad0d commit 1857ff9
Show file tree
Hide file tree
Showing 6 changed files with 488 additions and 27 deletions.
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) 2022 ActiveChooN
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT

name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:

Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18

- name: Verify dependencies
run: go mod verify

- name: Build
run: go build -v ./...

- name: Run tests
run: go test -v -vet=off ./...

Check:
runs-on: ubuntu-latest
needs: Build
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18

- name: Verify dependencies
run: go mod verify

- name: Run go vet
run: go vet ./...

- name: Run go staticcheck
uses: dominikh/[email protected]
with:
install-go: false

- name: Run go lint
uses: golangci/golangci-lint-action@v3

Test:
runs-on: ubuntu-latest
needs: Build
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18

- name: Verify dependencies
run: go mod verify

- name: Run tests
run: go test -v -vet=off ./...
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
<!--
Copyright (c) 2021 ActiveCHooN
Copyright (c) 2022 MagellanCL
This software is released under the MIT License.
https://opensource.org/licenses/MIT
-->

# Gin GORM filter
![GitHub](https://img.shields.io/github/license/Magellancl/gin-gorm-filter_v2)
![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Magellancl/gin-gorm-filter_v2/CI/master)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/Magellancl/gin-gorm-filter_v2)

Scope function for GORM queries provides easy filtering with query parameters

Fork of https://github.com/ActiveChooN/gin-gorm-filter

## Usage

```(shell)
go get github.com/magellancl/gin-gorm-filter
go get github.com/magellancl/gin-gorm-filter_v2
```

## Model definition
```go
type UserModel struct {
gorm.Model
Username string `gorm:"uniqueIndex" filter:"filterable"`
FullName string
FullName string `filter:"param:full_name"`
Role string `filter:"filterable"`
CreatedAt time.Time `filter:"filterable"`
UpdatedAt time.Time `filter:"filterable"`
}
```
`param` tag in that case defines custom column name for the query param
`param` tag defines custom column name and query param name.

## Controller Example
```go
Expand All @@ -45,8 +50,35 @@ func GetUsers(c *gin.Context) {
c.JSON(http.StatusOK, users)
}
```
Any filter combination can be used here `filter.PAGINATION|filter.ORDER_BY` e.g. **Important note:** GORM model should be initialize first for DB, otherwise filter and search won't work
Any filter combination can be used here `filter.PAGINATION|filter.ORDER_BY` e.g. **Important note:** GORM model should be initialized first for DB, otherwise filter won't work

## FILTER

Using the tag `filter:"filterable"` on your gorm object, and activating it with `filter.FILTER`, you can make a field filterable. The standard filter will use this format : `?username=john`.
You can use more complex filters with the separators <, >, >=, <=, !=. eg :
`?created_at>=2022-10-18&created_at<2022-10-21` (be careful of your timezone. You should be able to input any date format readable by your DBMS)
`?city!=grenoble`
`?price>10&created_at<2022-10-21`

## PAGINATE

Activating pagination with `filter.PAGINATE` will allow you to use the filters page and limit(eg : `?page=2&limit=50`). Limit maximum is 100, so you can request a maximum of 100 items at once. The default value is 20.
It will also renseign the following headers :
"X-Paginate-Items" -> total number of items
"X-Paginate-Pages" -> total number of pages
"X-Paginate-Current" -> current page
"X-Paginate-Limit" -> limit of items per page

## ORDER BY



## Request example
```(shell)
curl -X GET http://localhost:8080/users?page=1&limit=10&order_by=username&order_direction=asc&name=John
```

## TODO list
- [ ] Write tests for the lib with CI integration
- [x] Write tests for the lib with CI integration
- [ ] Add ILIKE integration for PostgreSQL database
- [X] Add other filters, like > or !=
17 changes: 9 additions & 8 deletions gin-gorm-filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ import (
)

type queryParams struct {
Filter string `form:"filter"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=20"`
All bool `form:"all,default=false"`
OrderBy string `form:"order_by,default=id"`
Desc bool `form:"desc,default=true"`
Filter string `form:"filter"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=20"`
All bool `form:"all,default=false"`
OrderBy string `form:"order_by,default=created_at"`
OrderDirection string `form:"order_direction,default=desc,oneof=desc asc"`
}

const (
FILTER = 2 // Filter response by column name values "filter={column_name}:{value}"
//SEARCH = 1 // NOT IMPLEMENTED // Filter response with LIKE query "search={search_phrase}"
FILTER = 2 // Filter response by column name values "{column_name}={value}"
PAGINATE = 4 // Paginate response with page and page_size
ORDER_BY = 8 // Order response by column name
ALL = 15 // Equivalent to SEARCH|FILTER|PAGINATE|ORDER_BY
Expand All @@ -42,7 +43,7 @@ var (
func orderBy(db *gorm.DB, params queryParams) *gorm.DB {
return db.Order(clause.OrderByColumn{
Column: clause.Column{Name: params.OrderBy},
Desc: params.Desc},
Desc: params.OrderDirection == "desc"},
)
}

Expand Down
174 changes: 174 additions & 0 deletions gin-gorm-filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) 2022 ActiveChooN
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package filter

import (
"database/sql"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

type User struct {
Id int64
Username string `filter:"searchable;filterable"`
FullName string `filter:"param:full_name;searchable"`
Email string `filter:"filterable"`
// This field is not filtered.
Password string
}

type TestSuite struct {
suite.Suite
db *gorm.DB
mock sqlmock.Sqlmock
}

func (s *TestSuite) SetupTest() {
var (
db *sql.DB
err error
)

db, s.mock, err = sqlmock.New()
s.NoError(err)
s.NotNil(db)
s.NotNil(s.mock)

dialector := postgres.New(postgres.Config{
DSN: "sqlmock_db_0",
DriverName: "postgres",
Conn: db,
PreferSimpleProtocol: true,
})

s.db, err = gorm.Open(dialector, &gorm.Config{})
require.NoError(s.T(), err)
require.NotNil(s.T(), s.db)
}

func (s *TestSuite) TearDownTest() {
db, err := s.db.DB()
require.NoError(s.T(), err)
db.Close()
}

// TestFiltersBasic is a test suite for basic filters functionality.
func (s *TestSuite) TestFiltersBasic() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "username=sampleUser",
},
}

s.mock.ExpectQuery(`^SELECT \* FROM "users" WHERE "username" = \$1`).
WithArgs("sampleUser").
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, FILTER)).Find(&users).Error
s.NoError(err)
}

// Filtering for a field that is not filtered should not be performed
func (s *TestSuite) TestFiltersNotFilterable() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "password=samplePassword",
},
}
s.mock.ExpectQuery(`^SELECT \* FROM "users" ORDER`).
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, FILTER|ORDER_BY)).Find(&users).Error
s.NoError(err)
}

// Filtering would not be applied if no config is provided.
func (s *TestSuite) TestFiltersNoFilterConfig() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "username=sampleUser",
},
}

s.mock.ExpectQuery(`^SELECT \* FROM "users"$`).
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, 0)).Find(&users).Error
s.NoError(err)
}

/* // search function is disabled for now
// TestFiltersSearchable is a test suite for searchable filters functionality.
func (s *TestSuite) TestFiltersSearchable() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "search=John",
},
}
s.mock.ExpectQuery(`^SELECT \* FROM "users" WHERE \("Username" LIKE \$1 OR "FullName" LIKE \$2\)`).
WithArgs("%John%", "%John%").
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, ALL)).Find(&users).Error
s.NoError(err)
}*/

// TestFiltersPaginateOnly is a test suite for pagination functionality.
func (s *TestSuite) TestFiltersPaginateOnly() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "page=2&limit=10",
},
}

s.mock.ExpectQuery(`^SELECT count\(\*\) FROM "users"`).WillReturnRows(sqlmock.NewRows([]string{"count"}))
s.mock.ExpectQuery(`^SELECT \* FROM "users" ORDER BY "created_at" DESC LIMIT 10 OFFSET 10$`).
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, ALL)).Find(&users).Error
s.NoError(err)
}

// TestFiltersOrderBy is a test suite for order by functionality.
func (s *TestSuite) TestFiltersOrderBy() {
var users []User
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{
RawQuery: "order_by=Email&order_direction=asc",
},
}

s.mock.ExpectQuery(`^SELECT \* FROM "users" ORDER BY "Email"$`).
WillReturnRows(sqlmock.NewRows([]string{"id", "Username", "FullName", "Email", "Password"}))
err := s.db.Model(&User{}).Scopes(FilterByQuery(ctx, ORDER_BY)).Find(&users).Error
s.NoError(err)
}

func TestRunSuite(t *testing.T) {
suite.Run(t, new(TestSuite))
}
Loading

0 comments on commit 1857ff9

Please sign in to comment.