filter

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2024 License: MIT Imports: 19 Imported by: 6

README

filter - Dynamic query params filters for Goyave

Version Build Status Coverage Status Go Reference

Compatible with Goyave v5 only. If you want to use the library for v4, use v0.6.0.

goyave.dev/filter allows powerful filtering using query parameters. Inspired by nestjsx/crud.

Usage

go get goyave.dev/filter

First, add the necessary query validation to your index route:

router.GlobalMiddleware(&parse.Middleware{}) // Don't forget the parse middleware!
router.Get("/users", user.Index).ValidateQuery(filter.Validation)

Then, create a new method in your repository and service:

// database/repository/user.go
import "goyave.dev/filter"

//...

func (r *User) Paginate(ctx context.Context, request *filter.Request) (*database.Paginator[*model.User], error) {
	users := []*model.User{}
	paginator, err := filter.Scope(session.DB(ctx, r.DB), request, &users)
	return paginator, errors.New(err)
}
// service/user/user.go
func (s *Service) Paginate(ctx context.Context, request *filter.Request) (*database.PaginatorDTO[*dto.User], error) {
	paginator, err := s.repository.Paginate(ctx, request)
	return typeutil.MustConvert[*database.PaginatorDTO[*dto.User]](paginator), errors.New(err)
}

From your controller, use filter.NewRequest() with the request's query to generate the filter request DTO:

// http/controller/user/user.go
func (ctrl *Controller) Index(response *goyave.Response, request *goyave.Request) {
	paginator, err := ctrl.userService.Paginate(request.Context(), filter.NewRequest(request.Query))
	if response.WriteDBError(err) {
		return
	}
	response.JSON(http.StatusOK, paginator)
}

And that's it! Now your front-end can add query parameters to filter as it wants.

You can also find records without paginating using ScopeUnpaginated():

var users []*model.User
tx := filter.ScopeUnpaginated(session.DB(ctx, r.DB), request, &users)

Note: ScopeUnpaginated() returns a *gorm.DB, not directly an error. To check for errors, use tx.Error.

Settings

You can disable certain features, or blacklist certain fields using filter.Settings:

settings := &filter.Settings[*model.User]{
	DisableFields: true, // Prevent usage of "fields"
	DisableFilter: true, // Prevent usage of "filter"
	DisableSort:   true, // Prevent usage of "sort"
	DisableJoin:   true, // Prevent usage of "join"

	// If not nil and not empty, and if the request is not providing any
	// sort, the request will be sorted according to the `*Sort` defined in this slice.
	// If `DisableSort` is enabled, this has no effect.
	DefaultSort: []*Sort{{Field: "name", Order: SortDescending}},

	// If true, the sort will wrap the value in `LOWER()` if it's a string, resulting in `ORDER BY LOWER(column)`.
	CaseInsensitiveSort: true, 

	FieldsSearch:   []string{"a", "b"},      // Optional, the fields used for the search feature
	SearchOperator: filter.Operators["$eq"], // Optional, operator used for the search feature, defaults to "$cont"

	Blacklist: filter.Blacklist{
		// Prevent selecting, sorting and filtering on these fields
		FieldsBlacklist: []string{"a", "b"},

		// Prevent joining these relations
		RelationsBlacklist: []string{"Relation"},

		Relations: map[string]*filter.Blacklist{
			// Blacklist settings to apply to this relation
			"Relation": &filter.Blacklist{
				FieldsBlacklist:    []string{"c", "d"},
				RelationsBlacklist: []string{"Parent"},
				Relations:          map[string]*filter.Blacklist{ /*...*/ },
				IsFinal:            true, // Prevent joining any child relation if true
			},
		},
	},
}
results := []*model.User{}
paginator, err := settings.Scope(session.DB(ctx, r.DB), request, &results)
Filter

?filter=field||$operator||value

Examples:

?filter=name||$cont||Jack (WHERE name LIKE "%Jack%")

You can add multiple filters. In that case, it is interpreted as an AND condition.

You can use OR conditions using ?or instead, or in combination:

?filter=name||$cont||Jack&or=name||$cont||John (WHERE (name LIKE %Jack% OR name LIKE "%John%"))
?filter=age||$eq||50&filter=name||$cont||Jack&or=name||$cont||John (WHERE ((age = 50 AND name LIKE "%Jack%") OR name LIKE "%John%"))

You can filter using columns from one-to-one relations ("has one" or "belongs to"):

?filter=Relation.name||$cont||Jack

If there is only one "or", it is considered as a regular filter:

?or=name||$cont||John (WHERE name LIKE "%John%")

If both "filter" and "or" are present, then they are interpreted as a combination of two AND groups compared with each other using OR:

?filter=age||$eq||50&filter=name||$cont||Jack&or=name||$cont||John&or=name||$cont||Doe
WHERE ((age = 50 AND name LIKE "%Jack%") OR (name LIKE "%John%" AND name LIKE "%Doe%"))

Note: All the filter conditions added to the SQL query are grouped (surrounded by parenthesis).

Operators
$eq =, equals
$ne <>, not equals
$gt >, greater than
$lt <, lower than
$gte >=, greater than or equals
$lte <=, lower than or equals
$starts LIKE val%, starts with
$ends LIKE %val, ends with
$cont LIKE %val%, contains
$excl NOT LIKE %val%, not contains
$in IN (val1, val2,...), in (accepts multiple values)
$notin NOT IN (val1, val2,...), in (accepts multiple values)
$isnull IS NULL, is NULL (doesn't accept value)
$notnull IS NOT NULL, not NULL (doesn't accept value)
$between BETWEEN val1 AND val2, between (accepts two values)

Search is similar to multiple or=column||$cont||value, but the column and operator are specified by the server instead of the client.

Specify the column using Settings:

settings := &filter.Settings{
	FieldsSearch: []string{"a", "b"},
	SearchOperator: filter.Operators["$eq"], // Optional, defaults to "$cont"
	//...
}

?search=John (WHERE (a LIKE "%John%" OR b LIKE "%John%"))

If you don't specify FieldsSearch, the query will search in all selectable fields.

Fields / Select

?fields=field1,field2

A comma-separated list of fields to select. If this field isn't provided, uses SELECT *.

Sort

?sort=column,ASC|DESC

Examples:

?sort=name,ASC
?sort=age,DESC

You can also sort by multiple fields.

?sort=age,DESC&sort=name,ASC

Join

?join=relation

Preload a relation. You can also only select the columns you need:

?join=relation||field1,field2

You can join multiple relations:

?join=profile||firstName,email&join=notifications||content&join=tasks

Pagination

Internally, goyave.dev/filter uses Goyave's Paginator.

?page=1&per_page=10

  • If page isn't given, the first page will be returned.
  • If per_page isn't given, the default page size will be used. This default value can be overridden by changing filter.DefaultPageSize.
  • Either way, the result is always paginated, even if those two parameters are missing.

Computed columns

Sometimes you need to work with a "virtual" column that is not stored in your database, but is computed using an SQL expression. A dynamic status depending on a date for example. In order to support the features of this library properly, you will have to add the expression to your model using the computed struct tag:

type MyModel struct {
	ID uint
	// ...
	StartDate time.Time
	Status    string `gorm:"->;-:migration" computed:"CASE WHEN ~~~ct~~~.start_date < NOW() THEN 'pending' ELSE 'started' END"`
}

Note: the ~~~ct~~~ is an indicator for the current table. It will be replaced by the correct table or relation name automatically. This allows the usage of computed fields in relations too, where joins are needed.

Tip: you can also use composition to avoid including the virtual column into your model:

type MyModel struct{
	ID uint
	// ...
	StartDate time.Time
}

type MyModelWithStatus struct{
	MyModel
	Status string `gorm:"->;-:migration" computed:"CASE WHEN ~~~ct~~~.start_date < NOW() THEN 'pending' ELSE 'started' END"` 
}

When using JSON columns, you can support filters on nested fields inside that JSON column using a computed column:

// This example is compatible with PostgreSQL.
// JSON processing may be different if you are using another database engine. 
type MyModel struct {
	ID            uint
	JSONColumn    datatypes.JSON
	SomeJSONField null.Int `gorm:"->;-:migration" computed:"(~~~ct~~~.json_column->>'fieldName')::int"`
}

It is important to make sure your JSON expression returns a value that has a type that matches the struct field to avoid DB errors. Database engines usually only return text types from JSON. If your field is a number, you'll have to cast it or you will get database errors when filtering on this field.

Security

  • Inputs are escaped to prevent SQL injections.
  • Fields are pre-processed and clients cannot request fields that don't exist. This prevents database errors. If a non-existing field is required, it is simply ignored. The same goes for sorts and joins. It is not possible to request a relation that doesn't exist.
  • Type-safety: in the same field pre-processing, the broad type of the field is checked against the database type (based on the model definition). This prevents database errors if the input cannot be converted to the column's type.
  • Foreign keys are always selected in joins to ensure associations can be assigned to parent model.
  • Be careful with bidirectional relations (for example an article is written by a user, and a user can have many articles). If you enabled both your models to preload these relations, the client can request them with an infinite depth (Articles.User.Articles.User...). To prevent this, it is advised to use the relation blacklist or IsFinal on the deepest requestable models. See the settings section for more details.

Tips

Model and DTO recommendations
  • Use json:",omitempty" on all DTO fields.
    • Note: using omitempty on slices will remove them from the json result if they are not nil and empty. There is currently no solution to this problem using the standard json package.
  • Don't include the foreign keys in the DTO.
  • Use *null.Time from the gopkg.in/guregu/null.v4 library instead of sql.NullTime.
  • Always specify gorm:"foreignKey", otherwise falls back to "ID".
  • Don't use gorm.Model and add the necessary fields manually. You get better control over json struct tags this way.
  • Use pointers for nullable relations and nullable fields that implement sql.Scanner (such as null.Time).
Type-safety

For non-native types that don't implement the driver.Valuer interface, you should always use the filterType struct tag. This struct tag enforces the field's recognized broad type for the type-safety conversion. It is also recommended to always add this tag when working with arrays. This tag is effective for the filter and search features.

Available broad types are:

  • text / text[]
  • enum / enum[]: use this with custom enum types to prevent "invalid input value" or "invalid operator" errors
  • bool / bool[]
  • int8 / int8[], int16 / int16[], int32 / int32[], int64 / int64[]
  • uint / uint[], uint16 / uint16[], uint32 / uint32[], uint64 / uint64[]
  • float32 / float32[], float64 / float64[]
  • time / time[]
  • -: unsupported data type. Fields tagged with - will be ignored in filters and search: no condition will be added to the WHERE clause.

If not provided, the type will be determined from GORM's data type. If GORM's data type is a custom type that is not directly supported by this library, the type will fall back to - (unsupported) and the field will be ignored in the filters.

If the type is supported but the user input cannot be used with the requested column, the built-in operators will generate a FALSE condition.

Example

type MyModel struct{
	ID uint
	// ...
	StartDate null.Time `filterType:"time"`
}
Static conditions

If you want to add static conditions (not automatically defined by the library), it is advised to group them like so:

users := []*model.User{}
db = session.DB(ctx, r.DB).Where(r.DB.Where("username LIKE ?", "%Miss%").Or("username LIKE ?", "%Ms.%"))
paginator, err := filter.Scope(db, request, &users)
Custom operators

You can add custom operators (or override existing ones) by modifying the filter.Operators map:

import (
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
	"goyave.dev/filter"
	"goyave.dev/goyave/v5/util/sqlutil"
)

// ...

filter.Operators["$cont"] = &filter.Operator{
	Function: func(tx *gorm.DB, f *filter.Filter, column string, dataType filter.DataType) *gorm.DB {
		if dataType != filter.DataTypeString {
			return f.Where("FALSE")
		}
		query := column + " LIKE ?"
		value := "%" + sqlutil.EscapeLike(f.Args[0]) + "%"
		return f.Where(tx, query, value)
	},
	RequiredArguments: 1,
}

filter.Operators["$eq"] = &filter.Operator{
	Function: func(tx *gorm.DB, f *filter.Filter, column string, dataType filter.DataType) *gorm.DB {
		if dataType.IsArray() {
			return f.Where("FALSE")
		}
		arg, ok := filter.ConvertToSafeType(f.Args[0], dataType)
		if !ok {
			return f.Where("FALSE")
		}
		query := fmt.Sprintf("%s = ?", column, op)
		return f.Where(tx, query, arg)
	},
	RequiredArguments: 1,
}
Array operators

Some database engines such as PostgreSQL provide operators for array operations (@>, &&, ...). You may encounter issue implementing these operators in your project because of GORM converting slices into records (("a", "b") instead of {"a", "b"}).

To fix this issue, you will have to implement your own variant of ConvertArgsToSafeType so it returns a pointer to a slice with a concrete type instead of []any. By sending a pointer to GORM, it won't try to render the slice itself and pass it directly to the underlying driver, which usually knows how to handle slices for the native types.

Example (using generics):

type argType interface {
	string | int64 | uint64 | float64 | bool
}

func init() {
	filter.Operators["$arrayin"] = &filter.Operator{
		Function: func (tx *gorm.DB, f *filter.Filter, column string, dataType filter.DataType) *gorm.DB {
			if !dataType.IsArray() {
				return f.Where("FALSE")
			}

			if dataType == filter.DataTypeEnumArray {
				column = fmt.Sprintf("CAST(%s as TEXT[])", column)
			}
		
			query := fmt.Sprintf("%s @> ?", column)
			switch dataType {
			case filter.DataTypeTextArray, filter.DataTypeEnumArray, filter.DataTypeTimeArray:
				return bindArrayArg[string](tx, query, f, dataType)
			case filter.DataTypeFloat32Array, filter.DataTypeFloat64Array:
				return bindArrayArg[float64](tx, query, f, dataType)
			case filter.DataTypeUint8Array, filter.DataTypeUint16Array, filter.DataTypeUint32Array, filter.DataTypeUint64Array:
				return bindArrayArg[uint64](tx, query, f, dataType)
			case filter.DataTypeInt8Array, filter.DataTypeInt16Array, filter.DataTypeInt32Array, filter.DataTypeInt64Array:
				return bindArrayArg[int64](tx, query, f, dataType)
			}
		
			// If you need to handle DataTypeBoolArray, use pgtype.BoolArray
			return f.Where("FALSE")
		},
		RequiredArguments: 1,
	}
}

func bindArrayArg[T argType](tx *gorm.DB, query string, f *filter.Filter, dataType filter.DataType) *gorm.DB {
	args, ok := convertArgsToSafeTypeArray[T](f.Args, dataType)
	if !ok {
		return f.Where("FALSE")
	}
	return f.Where(tx, query, args)
}

func convertArgsToSafeTypeArray[T argType](args []string, dataType filter.DataType) (*[]T, bool) {
	result := make([]T, 0, len(args))
	for _, arg := range args {
		a, ok := filter.ConvertToSafeType(arg, dataType)
		if !ok {
			return nil, false
		}
		result = append(result, a.(T))
	}

	return &result, true
}
Manual joins

Manual joins are supported and won't clash with joins that are automatically generated by the library. That means that if needed, you can write something like described in the following piece of code.

// database/repository/user.go
func (r *User) Paginate(ctx context.Context, request *filter.Request) (*database.Paginator[*model.User], error) {
	var users []*model.User
	db := session.DB(ctx, r.DB).Joins("Relation")
	paginator, err := filter.Scope(db, request, &users)
	return paginator, errors.New(err)
}
Upgrading from Goyave v4 to v5

User parameter are expected to be in the Query now. Update validation on your routes:

router.Get("/", ctrl.Index).ValidateQuery(filter.Validation)

If your route has custom query parameters as well, you can append the lib's validation:

func IndexRequest(r *goyave.Request) v.RuleSet {
	return append(v.RuleSet{
		{Path: "foo", Rules: v.List{v.Int(), v.Min(1)}},
	}, filter.Validation(r)...)
}

*filter.Settings now takes a generic parameter: a pointer to the target model. The slice given to the Scope method is expected to be a pointer to a slice of the same type.

settings := &filter.Settings[*model.User]{}

License

goyave.dev/filter is MIT Licensed. Copyright (c) 2024 Jérémy LAMBERT (SystemGlitch)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultPageSize the default pagination page size if the "per_page" query param
	// isn't provided.
	DefaultPageSize = 10
)
View Source
var (
	// Operators definitions. The key is the query representation of the operator, (e.g. "$eq").
	Operators = map[string]*Operator{
		"$eq":  {Function: basicComparison("="), RequiredArguments: 1},
		"$ne":  {Function: basicComparison("<>"), RequiredArguments: 1},
		"$gt":  {Function: basicComparison(">"), RequiredArguments: 1},
		"$lt":  {Function: basicComparison("<"), RequiredArguments: 1},
		"$gte": {Function: basicComparison(">="), RequiredArguments: 1},
		"$lte": {Function: basicComparison("<="), RequiredArguments: 1},
		"$starts": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeText && dataType != DataTypeEnum {
					return filter.Where(tx, "FALSE")
				}
				query := castEnumAsText(column, dataType) + " LIKE ?"
				value := sqlutil.EscapeLike(filter.Args[0]) + "%"
				return filter.Where(tx, query, value)
			},
			RequiredArguments: 1,
		},
		"$ends": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeText && dataType != DataTypeEnum {
					return filter.Where(tx, "FALSE")
				}
				query := castEnumAsText(column, dataType) + " LIKE ?"
				value := "%" + sqlutil.EscapeLike(filter.Args[0])
				return filter.Where(tx, query, value)
			},
			RequiredArguments: 1,
		},
		"$cont": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeText && dataType != DataTypeEnum {
					return filter.Where(tx, "FALSE")
				}
				query := castEnumAsText(column, dataType) + " LIKE ?"
				value := "%" + sqlutil.EscapeLike(filter.Args[0]) + "%"
				return filter.Where(tx, query, value)
			},
			RequiredArguments: 1,
		},
		"$excl": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeText && dataType != DataTypeEnum {
					return filter.Where(tx, "FALSE")
				}
				query := castEnumAsText(column, dataType) + " NOT LIKE ?"
				value := "%" + sqlutil.EscapeLike(filter.Args[0]) + "%"
				return filter.Where(tx, query, value)
			},
			RequiredArguments: 1,
		},
		"$in":    {Function: multiComparison("IN"), RequiredArguments: 1},
		"$notin": {Function: multiComparison("NOT IN"), RequiredArguments: 1},
		"$isnull": {
			Function: func(tx *gorm.DB, filter *Filter, column string, _ DataType) *gorm.DB {
				return filter.Where(tx, column+" IS NULL")
			},
			RequiredArguments: 0,
		},
		"$istrue": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeBool {
					return filter.Where(tx, "FALSE")
				}
				return filter.Where(tx, column+" IS TRUE")
			},
			RequiredArguments: 0,
		},
		"$isfalse": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType != DataTypeBool {
					return filter.Where(tx, "FALSE")
				}
				return filter.Where(tx, column+" IS FALSE")
			},
			RequiredArguments: 0,
		},
		"$notnull": {
			Function: func(tx *gorm.DB, filter *Filter, column string, _ DataType) *gorm.DB {
				return filter.Where(tx, column+" IS NOT NULL")
			},
			RequiredArguments: 0,
		},
		"$between": {
			Function: func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB {
				if dataType.IsArray() {
					return filter.Where(tx, "FALSE")
				}
				args, ok := ConvertArgsToSafeType(filter.Args[:2], dataType)
				if !ok {
					return filter.Where(tx, "FALSE")
				}
				query := castEnumAsText(column, dataType) + " BETWEEN ? AND ?"
				return filter.Where(tx, query, args...)
			},
			RequiredArguments: 2,
		},
	}
)
View Source
var Separator = "||"

Separator the separator used when parsing the query

Functions

func ConvertArgsToSafeType added in v0.6.0

func ConvertArgsToSafeType(args []string, dataType DataType) ([]any, bool)

ConvertArgsToSafeType converts a slice of string arguments to safe type that matches the column's data type in the same way as `ConvertToSafeType`. If any of the values in the given slice could not be converted, returns false.

func ConvertToSafeType added in v0.6.0

func ConvertToSafeType(arg string, dataType DataType) (any, bool)

ConvertToSafeType convert the string argument to a safe type that matches the column's data type. Returns false if the input could not be converted.

func Scope

func Scope[T any](db *gorm.DB, request *Request, dest *[]T) (*database.Paginator[T], error)

Scope using the default FilterSettings. See `FilterSettings.Scope()` for more details.

func ScopeUnpaginated added in v0.5.0

func ScopeUnpaginated[T any](db *gorm.DB, request *Request, dest *[]T) *gorm.DB

ScopeUnpaginated using the default FilterSettings. See `FilterSettings.ScopeUnpaginated()` for more details.

func Validation added in v0.7.0

func Validation(_ *goyave.Request) v.RuleSet

Validation returns a new RuleSet for query validation.

Types

type Blacklist

type Blacklist struct {
	Relations map[string]*Blacklist

	// FieldsBlacklist prevent the fields in this list to be selected or to
	// be used in filters and sorts.
	FieldsBlacklist []string
	// RelationsBlacklist prevent joining the relations in this list.
	RelationsBlacklist []string

	// IsFinal if true, prevent joining any relation
	IsFinal bool
}

Blacklist definition of blacklisted relations and fields.

type DataType added in v0.6.0

type DataType string

DataType is determined by the `filterType` struct tag (see `DataType` for available options). If not given, uses GORM's general DataType. Raw database data types are not supported so it is recommended to always specify a `filterType` in this scenario.

const (
	DataTypeText      DataType = "text"
	DataTypeTextArray DataType = "text[]"

	DataTypeEnum      DataType = "enum"
	DataTypeEnumArray DataType = "enum[]"

	DataTypeBool      DataType = "bool"
	DataTypeBoolArray DataType = "bool[]"

	DataTypeInt8       DataType = "int8"
	DataTypeInt8Array  DataType = "int8[]"
	DataTypeInt16      DataType = "int16"
	DataTypeInt16Array DataType = "int16[]"
	DataTypeInt32      DataType = "int32"
	DataTypeInt32Array DataType = "int32[]"
	DataTypeInt64      DataType = "int64"
	DataTypeInt64Array DataType = "int64[]"

	DataTypeUint8       DataType = "uint8"
	DataTypeUint8Array  DataType = "uint8[]"
	DataTypeUint16      DataType = "uint16"
	DataTypeUint16Array DataType = "uint16[]"
	DataTypeUint32      DataType = "uint32"
	DataTypeUint32Array DataType = "uint32[]"
	DataTypeUint64      DataType = "uint64"
	DataTypeUint64Array DataType = "uint64[]"

	DataTypeFloat32      DataType = "float32"
	DataTypeFloat32Array DataType = "float32[]"
	DataTypeFloat64      DataType = "float64"
	DataTypeFloat64Array DataType = "float64[]"

	DataTypeTime      DataType = "time"
	DataTypeTimeArray DataType = "time[]"

	// DataTypeUnsupported all fields with this tag will be ignored in filters and search.
	DataTypeUnsupported DataType = "-"
)

Supported DataTypes

func (DataType) IsArray added in v0.6.0

func (d DataType) IsArray() bool

IsArray returns true if this data type is an array.

type FieldsValidator added in v0.7.0

type FieldsValidator struct {
	v.BaseValidator
}

FieldsValidator splits the string field under validation by comma and trims every element.

func (*FieldsValidator) IsType added in v0.7.0

func (v *FieldsValidator) IsType() bool

IsType returns true

func (*FieldsValidator) Name added in v0.7.0

func (v *FieldsValidator) Name() string

Name returns the string name of the validator.

func (*FieldsValidator) Validate added in v0.7.0

func (v *FieldsValidator) Validate(ctx *v.Context) bool

Validate checks the field under validation satisfies this validator's criteria.

type Filter

type Filter struct {
	Field    string
	Operator *Operator
	Args     []string
	Or       bool
}

Filter structured representation of a filter query. The generic parameter is the type pointer type of the model.

func ParseFilter

func ParseFilter(filter string) (*Filter, error)

ParseFilter parse a string in format "field||$operator||value" and return a Filter struct. The filter string must satisfy the used operator's "RequiredArguments" constraint, otherwise an error is returned.

func (*Filter) Scope

func (f *Filter) Scope(blacklist Blacklist, sch *schema.Schema) (func(*gorm.DB) *gorm.DB, func(*gorm.DB) *gorm.DB)

Scope returns the GORM scope to use in order to apply this filter.

func (*Filter) Where

func (f *Filter) Where(tx *gorm.DB, query string, args ...any) *gorm.DB

Where applies a condition to given transaction, automatically taking the "Or" filter value into account.

type FilterValidator added in v0.7.0

type FilterValidator struct {
	v.BaseValidator
	Or bool
}

FilterValidator checks the `filter` format and converts it to `*Filter` struct.

func (*FilterValidator) IsType added in v0.7.0

func (v *FilterValidator) IsType() bool

IsType returns true

func (*FilterValidator) Name added in v0.7.0

func (v *FilterValidator) Name() string

Name returns the string name of the validator.

func (*FilterValidator) Validate added in v0.7.0

func (v *FilterValidator) Validate(ctx *v.Context) bool

Validate checks the field under validation satisfies this validator's criteria.

type Join

type Join struct {
	Relation string
	Fields   []string
	// contains filtered or unexported fields
}

Join structured representation of a join query.

func ParseJoin

func ParseJoin(join string) (*Join, error)

ParseJoin parse a string in format "relation||field1,field2,..." and return a Join struct.

func (*Join) Scopes

func (j *Join) Scopes(blacklist Blacklist, schema *schema.Schema) []func(*gorm.DB) *gorm.DB

Scopes returns the GORM scopes to use in order to apply this joint.

type JoinValidator added in v0.7.0

type JoinValidator struct {
	v.BaseValidator
}

JoinValidator checks the `sort` format and converts it to `*Join` struct.

func (*JoinValidator) IsType added in v0.7.0

func (v *JoinValidator) IsType() bool

IsType returns true

func (*JoinValidator) Name added in v0.7.0

func (v *JoinValidator) Name() string

Name returns the string name of the validator.

func (*JoinValidator) Validate added in v0.7.0

func (v *JoinValidator) Validate(ctx *v.Context) bool

Validate checks the field under validation satisfies this validator's criteria.

type Operator

type Operator struct {
	Function          func(tx *gorm.DB, filter *Filter, column string, dataType DataType) *gorm.DB
	RequiredArguments uint8
}

Operator used by filters to build the SQL query. The operator function modifies the GORM statement (most of the time by adding a WHERE condition) then returns the modified statement.

Operators may need arguments (e.g. "$eq", equals needs a value to compare the field to); RequiredArguments define the minimum number of arguments a client must send in order to use this operator in a filter. RequiredArguments is checked during Filter parsing.

Operators may return the given tx without change if they don't support the given dataType or add a condition that will always be false.

type Request added in v0.7.0

type Request struct {
	Search  typeutil.Undefined[string]
	Filter  typeutil.Undefined[[]*Filter]
	Or      typeutil.Undefined[[]*Filter]
	Sort    typeutil.Undefined[[]*Sort]
	Join    typeutil.Undefined[[]*Join]
	Fields  typeutil.Undefined[[]string]
	Page    typeutil.Undefined[int]
	PerPage typeutil.Undefined[int]
}

Request DTO for a filter query. Any non-present option will be ignored.

func NewRequest added in v0.7.0

func NewRequest(query map[string]any) *Request

NewRequest creates a filter request from an HTTP request's query. Uses the following entries in the query, expected to be validated:

  • search
  • filter
  • or
  • sort
  • join
  • fields
  • page
  • per_page

If a field in the query doesn't match the expected type (non-validated) for the filtering option, it will be ignored without an error.

type Search struct {
	Query    string
	Operator *Operator
	Fields   []string
}

Search structured representation of a search query.

func (*Search) Scope added in v0.2.0

func (s *Search) Scope(schema *schema.Schema) func(*gorm.DB) *gorm.DB

Scope returns the GORM scopes with the search query.

type Settings

type Settings[T any] struct {

	// DefaultSort if not nil and not empty, and if the request is not providing any
	// sort, the request will be sorted according to the `*Sort` defined in this slice.
	// If `DisableSort` is enabled, this has no effect.
	DefaultSort []*Sort

	// FieldsSearch allows search for these fields
	FieldsSearch []string
	// SearchOperator is used by the search scope, by default it use the $cont operator
	SearchOperator *Operator

	Blacklist

	// DisableFields ignore the "fields" query if true.
	DisableFields bool
	// DisableFilter ignore the "filter" query if true.
	DisableFilter bool
	// DisableSort ignore the "sort" query if true.
	DisableSort bool
	// DisableJoin ignore the "join" query if true.
	DisableJoin bool
	// DisableSearch ignore the "search" query if true.
	DisableSearch bool

	// CaseInsensitiveSort if true, the sort will wrap the value in `LOWER()` if it's a string,
	// resulting in `ORDER BY LOWER(column)`.
	CaseInsensitiveSort bool
}

Settings settings to disable certain features and/or blacklist fields and relations. The generic type is the pointer type of the model.

func (*Settings[T]) Scope

func (s *Settings[T]) Scope(db *gorm.DB, request *Request, dest *[]T) (*database.Paginator[T], error)

Scope apply all filters, sorts and joins defined in the request's data to the given `*gorm.DB` and process pagination. Returns the resulting `*database.Paginator`. The given request is expected to be validated using `ApplyValidation`.

func (*Settings[T]) ScopeUnpaginated added in v0.5.0

func (s *Settings[T]) ScopeUnpaginated(db *gorm.DB, request *Request, dest *[]T) *gorm.DB

ScopeUnpaginated apply all filters, sorts and joins defined in the request's data to the given `*gorm.DB` without any pagination. Returns the `*gorm.DB` result, which can be used to check for database errors. The records will be added in the given `dest` slice. The given request is expected to be validated using `ApplyValidation`.

type Sort

type Sort struct {
	Field string
	Order SortOrder
}

Sort structured representation of a sort query. The generic parameter is the type pointer type of the model.

func ParseSort

func ParseSort(sort string) (*Sort, error)

ParseSort parse a string in format "name,ASC" and return a Sort struct. The element after the comma (sort order) must have a value allowing it to be converted to SortOrder, otherwise an error is returned.

func (*Sort) Scope

func (s *Sort) Scope(blacklist Blacklist, schema *schema.Schema, caseInsensitive bool) func(*gorm.DB) *gorm.DB

Scope returns the GORM scope to use in order to apply sorting. If caseInsensitive is true, the column is wrapped in a `LOWER()` function.

type SortOrder

type SortOrder string

SortOrder the allowed strings for SQL "ORDER BY" clause.

const (
	// SortAscending "ORDER BY column ASC"
	SortAscending SortOrder = "ASC"
	// SortDescending "ORDER BY column DESC"
	SortDescending SortOrder = "DESC"
)

type SortValidator added in v0.7.0

type SortValidator struct {
	v.BaseValidator
}

SortValidator checks the `sort` format and converts it to `*Sort` struct.

func (*SortValidator) IsType added in v0.7.0

func (v *SortValidator) IsType() bool

IsType returns true

func (*SortValidator) Name added in v0.7.0

func (v *SortValidator) Name() string

Name returns the string name of the validator.

func (*SortValidator) Validate added in v0.7.0

func (v *SortValidator) Validate(ctx *v.Context) bool

Validate checks the field under validation satisfies this validator's criteria.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL