feat(filter): nesting
This commit is contained in:
parent
e43349618b
commit
76ed2cff5f
|
@ -90,6 +90,60 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
|||
return value.In(config.GetTimeZone()), err
|
||||
}
|
||||
|
||||
func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) {
|
||||
filter = &taskFilter{
|
||||
join: filterConcatAnd,
|
||||
}
|
||||
if f.Join == fexpr.JoinOr {
|
||||
filter.join = filterConcatOr
|
||||
}
|
||||
|
||||
var value string
|
||||
switch v := f.Item.(type) {
|
||||
case fexpr.Expr:
|
||||
filter.field = v.Left.Literal
|
||||
value = v.Right.Literal
|
||||
filter.comparator, err = getFilterComparatorFromOp(v.Op)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case []fexpr.ExprGroup:
|
||||
values := make([]*taskFilter, 0, len(v))
|
||||
for _, expression := range v {
|
||||
subfilter, err := parseFilterFromExpression(expression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, subfilter)
|
||||
}
|
||||
filter.value = values
|
||||
return
|
||||
}
|
||||
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if filter.field == "project" {
|
||||
filter.field = "project_id"
|
||||
}
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: value,
|
||||
}
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
|
||||
|
||||
if c.Filter == "" {
|
||||
|
@ -121,46 +175,10 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
|
|||
|
||||
filters = make([]*taskFilter, 0, len(parsedFilter))
|
||||
for _, f := range parsedFilter {
|
||||
|
||||
filter := &taskFilter{
|
||||
join: filterConcatAnd,
|
||||
}
|
||||
if f.Join == fexpr.JoinOr {
|
||||
filter.join = filterConcatOr
|
||||
}
|
||||
|
||||
var value string
|
||||
switch v := f.Item.(type) {
|
||||
case fexpr.Expr:
|
||||
filter.field = v.Left.Literal
|
||||
value = v.Right.Literal // TODO: nesting
|
||||
filter.comparator, err = getFilterComparatorFromOp(v.Op)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = validateTaskFieldComparator(filter.comparator)
|
||||
filter, err := parseFilterFromExpression(f)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if filter.field == "project" {
|
||||
filter.field = "project_id"
|
||||
}
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
Field: value,
|
||||
}
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
|
|
|
@ -851,6 +851,19 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "range and nesting",
|
||||
fields: fields{
|
||||
Filter: "(start_date > '2018-12-12T00:00:00+00:00' && start_date < '2018-12-13T00:00:00+00:00') || end_date > '2018-12-13T00:00:00+00:00'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "undone tasks only",
|
||||
fields: fields{
|
||||
|
@ -1090,8 +1103,42 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
fields: fields{
|
||||
Filter: "assignees ~ 'user'",
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
// Same as without any filter since the filter is ignored
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task11,
|
||||
task12,
|
||||
task15,
|
||||
task16,
|
||||
task17,
|
||||
task18,
|
||||
task19,
|
||||
task20,
|
||||
task21,
|
||||
task22,
|
||||
task23,
|
||||
task24,
|
||||
task25,
|
||||
task26,
|
||||
task27,
|
||||
task28,
|
||||
task29,
|
||||
task30,
|
||||
task31,
|
||||
task32,
|
||||
task33,
|
||||
task35,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -76,17 +76,21 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
|
|||
return
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (filterCond builder.Cond, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||
var dbFilters = make([]builder.Cond, 0, len(rawFilters))
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
for _, f := range opts.filters {
|
||||
for _, f := range rawFilters {
|
||||
|
||||
if nested, is := f.value.([]*taskFilter); is {
|
||||
nestedDBFilters, err := convertFiltersToDBFilterCond(nested, includeNulls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbFilters = append(dbFilters, nestedDBFilters)
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "reminders" {
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
|
@ -94,17 +98,17 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_reminders", filter))
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_reminders", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
if f.field == "assignees" {
|
||||
if f.comparator == taskFilterComparatorLike {
|
||||
return nil, totalCount, err
|
||||
return
|
||||
}
|
||||
filter, err := getFilterCond(&taskFilter{
|
||||
// recreating the struct here to avoid modifying it when reusing the opts struct
|
||||
|
@ -112,9 +116,9 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assigneeFilter := builder.In("user_id",
|
||||
|
@ -122,7 +126,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
From("users").
|
||||
Where(filter),
|
||||
)
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_assignees", assigneeFilter))
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_assignees", assigneeFilter))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -133,12 +137,12 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters = append(filters, getFilterCondForSeparateTable("label_tasks", filter))
|
||||
dbFilters = append(dbFilters, getFilterCondForSeparateTable("label_tasks", filter))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -149,9 +153,9 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
value: f.value,
|
||||
comparator: f.comparator,
|
||||
isNumeric: f.isNumeric,
|
||||
}, opts.filterIncludeNulls)
|
||||
}, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := builder.In(
|
||||
|
@ -161,15 +165,48 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
From("projects").
|
||||
Where(filter),
|
||||
)
|
||||
filters = append(filters, cond)
|
||||
dbFilters = append(dbFilters, cond)
|
||||
continue
|
||||
}
|
||||
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
filter, err := getFilterCond(f, includeNulls)
|
||||
if err != nil {
|
||||
return nil, totalCount, err
|
||||
return nil, err
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
dbFilters = append(dbFilters, filter)
|
||||
}
|
||||
|
||||
if len(dbFilters) > 0 {
|
||||
if len(dbFilters) == 1 {
|
||||
filterCond = dbFilters[0]
|
||||
} else {
|
||||
for i, f := range dbFilters {
|
||||
if len(dbFilters) > i+1 {
|
||||
switch rawFilters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterCond = builder.Or(filterCond, f, dbFilters[i+1])
|
||||
case filterConcatAnd:
|
||||
filterCond = builder.And(filterCond, f, dbFilters[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filterCond, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||
|
||||
orderby, err := getOrderByDBStatement(opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
filterCond, err := convertFiltersToDBFilterCond(opts.filters, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Then return all tasks for that projects
|
||||
|
@ -208,24 +245,6 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
favoritesCond = builder.In("id", favCond)
|
||||
}
|
||||
|
||||
var filterCond builder.Cond
|
||||
if len(filters) > 0 {
|
||||
if len(filters) == 1 {
|
||||
filterCond = filters[0]
|
||||
} else {
|
||||
for i, f := range filters {
|
||||
if len(filters) > i+1 {
|
||||
switch opts.filters[i+1].join {
|
||||
case filterConcatOr:
|
||||
filterCond = builder.Or(filterCond, f, filters[i+1])
|
||||
case filterConcatAnd:
|
||||
filterCond = builder.And(filterCond, f, filters[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
|
||||
|
||||
|
|
Loading…
Reference in New Issue