feat(filter): nesting
This commit is contained in:
parent
605a2131ba
commit
d30615d527
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue