feat(filter): nesting

This commit is contained in:
kolaente 2023-11-21 19:18:01 +01:00
parent e43349618b
commit 76ed2cff5f
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
}
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)
}

View File

@ -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,
},
{

View File

@ -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)