package query import ( "errors" "reflect" sq "github.com/Masterminds/squirrel" "github.com/caos/zitadel/internal/domain" "github.com/lib/pq" ) type SearchResponse struct { Count uint64 *LatestSequence } type SearchRequest struct { Offset uint64 Limit uint64 SortingColumn Column Asc bool } func (req *SearchRequest) toQuery(query sq.SelectBuilder) sq.SelectBuilder { if req.Offset > 0 { query = query.Offset(req.Offset) } if req.Limit > 0 { query = query.Limit(req.Limit) } if !req.SortingColumn.isZero() { clause := "LOWER(" + sqlPlaceholder + ")" if !req.Asc { clause += " DESC" } query = query.OrderByClause(clause, req.SortingColumn.identifier()) } return query } const sqlPlaceholder = "?" type SearchQuery interface { toQuery(sq.SelectBuilder) sq.SelectBuilder } type NotNullQuery struct { Column Column } func NewNotNullQuery(col Column) (*NotNullQuery, error) { if col.isZero() { return nil, ErrMissingColumn } return &NotNullQuery{ Column: col, }, nil } func (q *NotNullQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query.Where(sq.NotEq{q.Column.identifier(): nil}) } type TextQuery struct { Column Column Text string Compare TextComparison } var ( ErrInvalidCompare = errors.New("invalid compare") ErrMissingColumn = errors.New("missing column") ErrInvalidNumber = errors.New("value is no number") ) func NewTextQuery(col Column, value string, compare TextComparison) (*TextQuery, error) { if compare < 0 || compare >= textCompareMax { return nil, ErrInvalidCompare } if col.isZero() { return nil, ErrMissingColumn } return &TextQuery{ Column: col, Text: value, Compare: compare, }, nil } func (q *TextQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { where, args := q.comp() return query.Where(where, args...) } func (s *TextQuery) comp() (comparison interface{}, args []interface{}) { switch s.Compare { case TextEquals: return sq.Eq{s.Column.identifier(): s.Text}, nil case TextEqualsIgnoreCase: return sq.ILike{s.Column.identifier(): s.Text}, nil case TextStartsWith: return sq.Like{s.Column.identifier(): s.Text + "%"}, nil case TextStartsWithIgnoreCase: return sq.ILike{s.Column.identifier(): s.Text + "%"}, nil case TextEndsWith: return sq.Like{s.Column.identifier(): "%" + s.Text}, nil case TextEndsWithIgnoreCase: return sq.ILike{s.Column.identifier(): "%" + s.Text}, nil case TextContains: return sq.Like{s.Column.identifier(): "%" + s.Text + "%"}, nil case TextContainsIgnoreCase: return sq.ILike{s.Column.identifier(): "%" + s.Text + "%"}, nil case TextListContains: return s.Column.identifier() + " @> ? ", []interface{}{pq.StringArray{s.Text}} } return nil, nil } type TextComparison int const ( TextEquals TextComparison = iota TextEqualsIgnoreCase TextStartsWith TextStartsWithIgnoreCase TextEndsWith TextEndsWithIgnoreCase TextContains TextContainsIgnoreCase TextListContains textCompareMax ) //Deprecated: Use TextComparison, will be removed as soon as all calls are changed to query func TextComparisonFromMethod(m domain.SearchMethod) TextComparison { switch m { case domain.SearchMethodEquals: return TextEquals case domain.SearchMethodEqualsIgnoreCase: return TextEqualsIgnoreCase case domain.SearchMethodStartsWith: return TextStartsWith case domain.SearchMethodStartsWithIgnoreCase: return TextStartsWithIgnoreCase case domain.SearchMethodContains: return TextContains case domain.SearchMethodContainsIgnoreCase: return TextContainsIgnoreCase case domain.SearchMethodEndsWith: return TextEndsWith case domain.SearchMethodEndsWithIgnoreCase: return TextEndsWithIgnoreCase case domain.SearchMethodListContains: return TextListContains default: return textCompareMax } } type NumberQuery struct { Column Column Number interface{} Compare NumberComparison } func NewNumberQuery(c Column, value interface{}, compare NumberComparison) (*NumberQuery, error) { if compare < 0 || compare >= numberCompareMax { return nil, ErrInvalidCompare } if c.isZero() { return nil, ErrMissingColumn } switch reflect.TypeOf(value).Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: //everything fine default: return nil, ErrInvalidNumber } return &NumberQuery{ Column: c, Number: value, Compare: compare, }, nil } func (q *NumberQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { where, args := q.comp() return query.Where(where, args...) } func (s *NumberQuery) comp() (comparison interface{}, args []interface{}) { switch s.Compare { case NumberEquals: return sq.Eq{s.Column.identifier(): s.Number}, nil case NumberNotEquals: return sq.NotEq{s.Column.identifier(): s.Number}, nil case NumberLess: return sq.Lt{s.Column.identifier(): s.Number}, nil case NumberGreater: return sq.Gt{s.Column.identifier(): s.Number}, nil case NumberListContains: return s.Column.identifier() + " @> ? ", []interface{}{pq.Array(s.Number)} } return nil, nil } type NumberComparison int const ( NumberEquals NumberComparison = iota NumberNotEquals NumberLess NumberGreater NumberListContains numberCompareMax ) //Deprecated: Use NumberComparison, will be removed as soon as all calls are changed to query func NumberComparisonFromMethod(m domain.SearchMethod) NumberComparison { switch m { case domain.SearchMethodEquals: return NumberEquals case domain.SearchMethodNotEquals: return NumberNotEquals case domain.SearchMethodGreaterThan: return NumberGreater case domain.SearchMethodLessThan: return NumberLess case domain.SearchMethodListContains: return NumberListContains default: return numberCompareMax } } type ListQuery struct { Column Column List []interface{} Compare ListComparison } func NewListQuery(column Column, value []interface{}, compare ListComparison) (*ListQuery, error) { if compare < 0 || compare >= listCompareMax { return nil, ErrInvalidCompare } if column.isZero() { return nil, ErrMissingColumn } return &ListQuery{ Column: column, List: value, Compare: compare, }, nil } func (q *ListQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { where, args := q.comp() return query.Where(where, args...) } func (s *ListQuery) comp() (interface{}, []interface{}) { switch s.Compare { case ListIn: return sq.Eq{s.Column.identifier(): s.List}, nil } return nil, nil } type ListComparison int const ( ListIn ListComparison = iota listCompareMax ) func ListComparisonFromMethod(m domain.SearchMethod) ListComparison { switch m { case domain.SearchMethodEquals: return ListIn default: return listCompareMax } } type BoolQuery struct { Column Column Value bool } func NewBoolQuery(c Column, value bool) (*BoolQuery, error) { return &BoolQuery{ Column: c, Value: value, }, nil } func (q *BoolQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { where, args := q.comp() return query.Where(where, args...) } func (s *BoolQuery) comp() (comparison interface{}, args []interface{}) { return sq.Eq{s.Column.identifier(): s.Value}, nil } var ( //countColumn represents the default counter for search responses countColumn = Column{ name: "COUNT(*) OVER ()", } //uniqueColumn shows if there are any results uniqueColumn = Column{ name: "COUNT(*) = 0", } ) type table struct { name string alias string } func (t table) setAlias(a string) table { t.alias = a return t } func (t table) identifier() string { if t.alias == "" { return t.name } return t.name + " as " + t.alias } func (t table) isZero() bool { return t.name == "" } type Column struct { name string table table } func (c Column) identifier() string { if c.table.alias != "" { return c.table.alias + "." + c.name } if c.table.name != "" { return c.table.name + "." + c.name } return c.name } func (c Column) setTable(t table) Column { c.table = t return c } func (c Column) isZero() bool { return c.table.isZero() || c.name == "" } func join(join, from Column) string { return join.table.identifier() + " ON " + from.identifier() + " = " + join.identifier() }