feat(filter): nesting

This commit is contained in:
kolaente 2023-11-21 19:18:01 +01:00
parent 605a2131ba
commit d30615d527
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 167 additions and 83 deletions

View File

@ -90,6 +90,60 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
return value.In(config.GetTimeZone()), err 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) { func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
if c.Filter == "" { if c.Filter == "" {
@ -121,46 +175,10 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
filters = make([]*taskFilter, 0, len(parsedFilter)) filters = make([]*taskFilter, 0, len(parsedFilter))
for _, f := range parsedFilter { for _, f := range parsedFilter {
filter, err := parseFilterFromExpression(f)
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)
if err != nil { 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) filters = append(filters, filter)
} }

View File

@ -851,6 +851,19 @@ func TestTaskCollection_ReadAll(t *testing.T) {
}, },
wantErr: false, 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", name: "undone tasks only",
fields: fields{ fields: fields{
@ -1090,8 +1103,42 @@ func TestTaskCollection_ReadAll(t *testing.T) {
fields: fields{ fields: fields{
Filter: "assignees ~ 'user'", Filter: "assignees ~ 'user'",
}, },
args: defaultArgs, args: defaultArgs,
want: []*Task{}, 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, wantErr: false,
}, },
{ {

View File

@ -76,17 +76,21 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
return return
} }
//nolint:gocyclo func convertFiltersToDBFilterCond(rawFilters []*taskFilter, includeNulls bool) (filterCond builder.Cond, err error) {
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
orderby, err := getOrderByDBStatement(opts) var dbFilters = make([]builder.Cond, 0, len(rawFilters))
if err != nil {
return nil, 0, err
}
var filters = make([]builder.Cond, 0, len(opts.filters))
// To still find tasks with nil values, we exclude 0s when comparing with >/< values. // 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" { if f.field == "reminders" {
filter, err := getFilterCond(&taskFilter{ filter, err := getFilterCond(&taskFilter{
// recreating the struct here to avoid modifying it when reusing the opts struct // 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, value: f.value,
comparator: f.comparator, comparator: f.comparator,
isNumeric: f.isNumeric, isNumeric: f.isNumeric,
}, opts.filterIncludeNulls) }, includeNulls)
if err != nil { 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 continue
} }
if f.field == "assignees" { if f.field == "assignees" {
if f.comparator == taskFilterComparatorLike { if f.comparator == taskFilterComparatorLike {
return nil, totalCount, err return
} }
filter, err := getFilterCond(&taskFilter{ filter, err := getFilterCond(&taskFilter{
// recreating the struct here to avoid modifying it when reusing the opts struct // 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, value: f.value,
comparator: f.comparator, comparator: f.comparator,
isNumeric: f.isNumeric, isNumeric: f.isNumeric,
}, opts.filterIncludeNulls) }, includeNulls)
if err != nil { if err != nil {
return nil, totalCount, err return nil, err
} }
assigneeFilter := builder.In("user_id", assigneeFilter := builder.In("user_id",
@ -122,7 +126,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
From("users"). From("users").
Where(filter), Where(filter),
) )
filters = append(filters, getFilterCondForSeparateTable("task_assignees", assigneeFilter)) dbFilters = append(dbFilters, getFilterCondForSeparateTable("task_assignees", assigneeFilter))
continue continue
} }
@ -133,12 +137,12 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
value: f.value, value: f.value,
comparator: f.comparator, comparator: f.comparator,
isNumeric: f.isNumeric, isNumeric: f.isNumeric,
}, opts.filterIncludeNulls) }, includeNulls)
if err != nil { 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 continue
} }
@ -149,9 +153,9 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
value: f.value, value: f.value,
comparator: f.comparator, comparator: f.comparator,
isNumeric: f.isNumeric, isNumeric: f.isNumeric,
}, opts.filterIncludeNulls) }, includeNulls)
if err != nil { if err != nil {
return nil, totalCount, err return nil, err
} }
cond := builder.In( cond := builder.In(
@ -161,15 +165,48 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
From("projects"). From("projects").
Where(filter), Where(filter),
) )
filters = append(filters, cond) dbFilters = append(dbFilters, cond)
continue continue
} }
filter, err := getFilterCond(f, opts.filterIncludeNulls) filter, err := getFilterCond(f, includeNulls)
if err != nil { 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 // 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) 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) limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond) cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)