From bcd414b5e7f187a46a22b37656020aa6cef67799 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 22 Nov 2023 11:14:06 +0100 Subject: [PATCH] feat(filters): make new filter syntax work with Typesense --- go.mod | 3 +- pkg/models/task_search.go | 112 +++++++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 0212e7ebe..54270b6a4 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/disintegration/imaging v1.6.2 github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 github.com/gabriel-vasile/mimetype v1.4.3 + github.com/ganigeorgiev/fexpr v0.4.0 github.com/getsentry/sentry-go v0.27.0 github.com/go-sql-driver/mysql v1.7.1 github.com/go-testfixtures/testfixtures/v3 v3.10.0 @@ -134,7 +135,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -162,6 +162,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tj/assert v0.0.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index b66021f18..66949dbfb 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -315,41 +315,23 @@ func convertFilterValues(value interface{}) string { return "" } -func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) { +// Parsing and rebuilding the filter for Typesense has the advantage that we have more control over +// what Typesense finally gets to see. +func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string, err error) { - var sortbyFields []string - for i, param := range opts.sortby { - // Validate the params - if err := param.validate(); err != nil { - return nil, totalCount, err + filters := []string{} + + for _, f := range rawFilters { + + if nested, is := f.value.([]*taskFilter); is { + nestedDBFilters, err := convertParsedFilterToTypesense(nested) + if err != nil { + return "", err + } + filters = append(filters, "("+nestedDBFilters+")") + continue } - // Typesense does not allow sorting by ID, so we sort by created timestamp instead - if param.sortBy == "id" { - param.sortBy = "created" - } - - sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String()) - - if i == 2 { - // Typesense supports up to 3 sorting parameters - // https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters - break - } - } - - sortby := strings.Join(sortbyFields, ",") - - projectIDStrings := []string{} - for _, id := range opts.projectIDs { - projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10)) - } - filterBy := []string{ - "project_id: [" + strings.Join(projectIDStrings, ", ") + "]", - } - - for _, f := range opts.parsedFilters { - if f.field == "reminders" { f.field = "reminders.reminder" } @@ -362,6 +344,10 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, f.field = "labels.id" } + if f.field == "project" { + f.field = "project_id" + } + filter := f.field switch f.comparator { @@ -393,7 +379,67 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, filter += "]" } - filterBy = append(filterBy, filter) + filters = append(filters, filter) + } + + if len(filters) > 0 { + if len(filters) == 1 { + filterBy = filters[0] + } else { + for i, f := range filters { + if len(filters) > i+1 { + switch rawFilters[i+1].join { + case filterConcatOr: + filterBy = f + " || " + filters[i+1] + case filterConcatAnd: + filterBy = f + " && " + filters[i+1] + } + } + } + } + } + + return +} + +func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) { + + var sortbyFields []string + for i, param := range opts.sortby { + // Validate the params + if err := param.validate(); err != nil { + return nil, totalCount, err + } + + // Typesense does not allow sorting by ID, so we sort by created timestamp instead + if param.sortBy == "id" { + param.sortBy = "created" + } + + sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String()) + + if i == 2 { + // Typesense supports up to 3 sorting parameters + // https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters + break + } + } + + sortby := strings.Join(sortbyFields, ",") + + projectIDStrings := []string{} + for _, id := range opts.projectIDs { + projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10)) + } + + filter, err := convertParsedFilterToTypesense(opts.parsedFilters) + if err != nil { + return nil, 0, err + } + + filterBy := []string{ + "project_id: [" + strings.Join(projectIDStrings, ", ") + "]", + "(" + filter + ")", } ////////////////