diff --git a/pkg/db/fixtures/list.yml b/pkg/db/fixtures/list.yml index 20ea7b6eecb..7cbfadc8b99 100644 --- a/pkg/db/fixtures/list.yml +++ b/pkg/db/fixtures/list.yml @@ -171,6 +171,7 @@ namespace_id: 14 updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 +# User 1 does not have access to this list - id: 20 title: Test20 diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 3b3985b53af..0aedeb30b7e 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -91,9 +91,9 @@ func validateTaskField(fieldName string) error { // @Param s query string false "Search tasks by task text." // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." -// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties except `labels`, `assignees`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match." +// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match." // @Param filter_value query string false "The value to filter for." -// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals` and `like`. Defaults to `equals`" +// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`" // @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`." // @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." // @Security JWTKeyAuth diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index d0022fbfd00..09b73556ff6 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "time" "code.vikunja.io/api/pkg/config" @@ -40,6 +41,7 @@ const ( taskFilterComparatorLessEquals taskFilterComparator = "<=" taskFilterComparatorNotEquals taskFilterComparator = "!=" taskFilterComparatorLike taskFilterComparator = "like" + taskFilterComparatorIn taskFilterComparator = "in" ) type taskFilter struct { @@ -89,7 +91,7 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err // Cast the field value to its native type if len(c.FilterValue) > i { - filter.value, err = getNativeValueForTaskField(filter.field, c.FilterValue[i]) + filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i]) if err != nil { return nil, ErrInvalidTaskFilterValue{ Value: filter.field, @@ -113,7 +115,8 @@ func validateTaskFieldComparator(comparator taskFilterComparator) error { taskFilterComparatorLess, taskFilterComparatorLessEquals, taskFilterComparatorNotEquals, - taskFilterComparatorLike: + taskFilterComparatorLike, + taskFilterComparatorIn: return nil case taskFilterComparatorInvalid: fallthrough @@ -138,42 +141,75 @@ func getFilterComparatorFromString(comparator string) (taskFilterComparator, err return taskFilterComparatorNotEquals, nil case "like": return taskFilterComparatorLike, nil + case "in": + return taskFilterComparatorIn, nil default: return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)} } } -func getNativeValueForTaskField(fieldName, value string) (nativeValue interface{}, err error) { - field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(strcase.ToCamel(fieldName)) - if !ok { - return nil, ErrInvalidTaskField{TaskField: fieldName} - } - +func getValueForField(field reflect.StructField, rawValue string) (value interface{}, err error) { switch field.Type.Kind() { case reflect.Int64: - nativeValue, err = strconv.ParseInt(value, 10, 64) + value, err = strconv.ParseInt(rawValue, 10, 64) case reflect.Float64: - nativeValue, err = strconv.ParseFloat(value, 64) + value, err = strconv.ParseFloat(rawValue, 64) case reflect.String: - nativeValue = value + value = rawValue case reflect.Bool: - nativeValue, err = strconv.ParseBool(value) + value, err = strconv.ParseBool(rawValue) case reflect.Struct: if field.Type == schemas.TimeType { - nativeValue, err = time.Parse(time.RFC3339, value) - nativeValue = nativeValue.(time.Time).In(config.GetTimeZone()) + value, err = time.Parse(time.RFC3339, rawValue) + value = value.(time.Time).In(config.GetTimeZone()) } case reflect.Slice: - t := reflect.SliceOf(schemas.TimeType) - if t != nil { - nativeValue, err = time.Parse(time.RFC3339, value) - nativeValue = nativeValue.(time.Time).In(config.GetTimeZone()) + // If this is a slice of pointers we're dealing with some property which is a relation + // In that case we don't really care about what the actual type is, we just cast the value to an + // int64 since we need the id - yes, this assumes we only ever have int64 IDs, but this is fine. + if field.Type.Elem().Kind() == reflect.Ptr { + value, err = strconv.ParseInt(rawValue, 10, 64) + return + } + + // There are probably better ways to do this - please let me know if you have one. + if field.Type.Elem().String() == "time.Time" { + value, err = time.Parse(time.RFC3339, rawValue) + value = value.(time.Time).In(config.GetTimeZone()) return } fallthrough default: - panic(fmt.Errorf("unrecognized filter type %s for field %s, value %s", field.Type.String(), fieldName, value)) + panic(fmt.Errorf("unrecognized filter type %s for field %s, value %s", field.Type.String(), field.Name, value)) } return } + +func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) { + + var realFieldName = strcase.ToCamel(fieldName) + if strings.ToLower(fieldName) == "id" { + realFieldName = "ID" + } + + field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName) + if !ok { + return nil, ErrInvalidTaskField{TaskField: fieldName} + } + + if comparator == taskFilterComparatorIn { + vals := strings.Split(value, ",") + valueSlice := []interface{}{} + for _, val := range vals { + v, err := getValueForField(field, val) + if err != nil { + return nil, err + } + valueSlice = append(valueSlice, v) + } + return valueSlice, nil + } + + return getValueForField(field, value) +} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index c68f2d67d3f..3a3623d8390 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -892,6 +892,46 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "filter in", + fields: fields{ + FilterBy: []string{"id"}, + FilterValue: []string{"1,2,34"}, // Task 34 is forbidden for user 1 + FilterComparator: []string{"in"}, + }, + args: defaultArgs, + want: []*Task{ + task1, + task2, + }, + wantErr: false, + }, + { + name: "filter assignees", + fields: fields{ + FilterBy: []string{"assignees"}, + FilterValue: []string{"1"}, + FilterComparator: []string{"equals"}, + }, + args: defaultArgs, + want: []*Task{ + task30, + }, + wantErr: false, + }, + { + name: "filter assignees in", + fields: fields{ + FilterBy: []string{"assignees"}, + FilterValue: []string{"1,2"}, + FilterComparator: []string{"in"}, + }, + args: defaultArgs, + want: []*Task{ + task30, + }, + wantErr: false, + }, } for _, tt := range tests { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 03489b12c89..f52352d6fa4 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -142,12 +142,13 @@ type taskOptions struct { // @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned." // @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." // @Param s query string false "Search tasks by task text." -// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." +// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." -// @Param filter_by query string false "The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match." +// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match." // @Param filter_value query string false "The value to filter for." -// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less` and `less_equals`. Defaults to `equals`" +// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`" // @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`." +// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." // @Security JWTKeyAuth // @Success 200 {array} models.Task "The tasks" // @Failure 500 {object} models.Message "Internal error" @@ -176,6 +177,8 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err return nil, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value} } cond = &builder.Like{f.field, "%" + val + "%"} + case taskFilterComparatorIn: + cond = builder.In(f.field, f.value) case taskFilterComparatorInvalid: // Nothing to do } @@ -187,6 +190,24 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err return } +func getFilterCondForSeparateTable(table string, concat taskFilterConcatinator, conds []builder.Cond) builder.Cond { + var filtercond builder.Cond + if concat == filterConcatOr { + filtercond = builder.Or(conds...) + } + if concat == filterConcatAnd { + filtercond = builder.And(conds...) + } + + return builder.In( + "id", + builder. + Select("task_id"). + From(table). + Where(filtercond), + ) +} + //nolint:gocyclo func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) { @@ -246,8 +267,9 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] } } - // Reminder filters need a special treatment since they are in a separate database + // Some filters need a special treatment since they are in a separate table reminderFilters := []builder.Cond{} + assigneeFilters := []builder.Cond{} var filters = make([]builder.Cond, 0, len(opts.filters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. @@ -262,6 +284,16 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] continue } + if f.field == "assignees" { + f.field = "user_id" + filter, err := getFilterCond(f, opts.filterIncludeNulls) + if err != nil { + return nil, 0, 0, err + } + assigneeFilters = append(assigneeFilters, filter) + continue + } + filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { return nil, 0, 0, err @@ -315,22 +347,11 @@ func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks [] } if len(reminderFilters) > 0 { - var filtercond builder.Cond - if opts.filterConcat == filterConcatOr { - filtercond = builder.Or(reminderFilters...) - } - if opts.filterConcat == filterConcatAnd { - filtercond = builder.And(reminderFilters...) - } - reminderFilter := builder.In( - "id", - builder. - Select("task_id"). - From("task_reminders"). - Where(filtercond), - ) + filters = append(filters, getFilterCondForSeparateTable("task_reminders", opts.filterConcat, reminderFilters)) + } - filters = append(filters, reminderFilter) + if len(assigneeFilters) > 0 { + filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilters)) } query = query.Where(listCond) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index d76499811f8..734509c2376 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1892,7 +1892,7 @@ var doc = `{ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except ` + "`" + `labels` + "`" + `, ` + "`" + `assignees` + "`" + `, ` + "`" + `list` + "`" + ` and ` + "`" + `namespace` + "`" + `. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", + "description": "The name of the field to filter by. Allowed values are all task properties except ` + "`" + `labels` + "`" + `, ` + "`" + `list` + "`" + ` and ` + "`" + `namespace` + "`" + `. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "name": "filter_by", "in": "query" }, @@ -1904,7 +1904,7 @@ var doc = `{ }, { "type": "string", - "description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + `, ` + "`" + `less_equals` + "`" + ` and ` + "`" + `like` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `", + "description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + `, ` + "`" + `less_equals` + "`" + `, ` + "`" + `like` + "`" + ` and ` + "`" + `in` + "`" + `. ` + "`" + `in` + "`" + ` expects comma-separated values in ` + "`" + `filter_value` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `", "name": "filter_comparator", "in": "query" }, @@ -3994,7 +3994,7 @@ var doc = `{ }, { "type": "string", - "description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with ` + "`" + `order_by` + "`" + `. Possible values to sort by are ` + "`" + `id` + "`" + `, ` + "`" + `text` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `done` + "`" + `, ` + "`" + `done_at` + "`" + `, ` + "`" + `due_date` + "`" + `, ` + "`" + `created_by_id` + "`" + `, ` + "`" + `list_id` + "`" + `, ` + "`" + `repeat_after` + "`" + `, ` + "`" + `priority` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, ` + "`" + `hex_color` + "`" + `, ` + "`" + `percent_done` + "`" + `, ` + "`" + `uid` + "`" + `, ` + "`" + `created` + "`" + `, ` + "`" + `updated` + "`" + `. Default is ` + "`" + `id` + "`" + `.", + "description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with ` + "`" + `order_by` + "`" + `. Possible values to sort by are ` + "`" + `id` + "`" + `, ` + "`" + `title` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `done` + "`" + `, ` + "`" + `done_at` + "`" + `, ` + "`" + `due_date` + "`" + `, ` + "`" + `created_by_id` + "`" + `, ` + "`" + `list_id` + "`" + `, ` + "`" + `repeat_after` + "`" + `, ` + "`" + `priority` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, ` + "`" + `hex_color` + "`" + `, ` + "`" + `percent_done` + "`" + `, ` + "`" + `uid` + "`" + `, ` + "`" + `created` + "`" + `, ` + "`" + `updated` + "`" + `. Default is ` + "`" + `id` + "`" + `.", "name": "sort_by", "in": "query" }, @@ -4006,7 +4006,7 @@ var doc = `{ }, { "type": "string", - "description": "The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", + "description": "The name of the field to filter by. Allowed values are all task properties except ` + "`" + `labels` + "`" + `, ` + "`" + `list` + "`" + ` and ` + "`" + `namespace` + "`" + `. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "name": "filter_by", "in": "query" }, @@ -4018,7 +4018,7 @@ var doc = `{ }, { "type": "string", - "description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + ` and ` + "`" + `less_equals` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `", + "description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + `, ` + "`" + `less_equals` + "`" + `, ` + "`" + `like` + "`" + ` and ` + "`" + `in` + "`" + `. ` + "`" + `in` + "`" + ` expects comma-separated values in ` + "`" + `filter_value` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `", "name": "filter_comparator", "in": "query" }, @@ -4027,6 +4027,12 @@ var doc = `{ "description": "The concatinator to use for filters. Available values are ` + "`" + `and` + "`" + ` or ` + "`" + `or` + "`" + `. Defaults to ` + "`" + `or` + "`" + `.", "name": "filter_concat", "in": "query" + }, + { + "type": "string", + "description": "If set to true the result will include filtered fields whose value is set to ` + "`" + `null` + "`" + `. Available values are ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. Defaults to ` + "`" + `false` + "`" + `.", + "name": "filter_include_nulls", + "in": "query" } ], "responses": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index f9fb78832d7..f1579ca9be4 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1875,7 +1875,7 @@ }, { "type": "string", - "description": "The name of the field to filter by. Allowed values are all task properties except `labels`, `assignees`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", + "description": "The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "name": "filter_by", "in": "query" }, @@ -1887,7 +1887,7 @@ }, { "type": "string", - "description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals` and `like`. Defaults to `equals`", + "description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`", "name": "filter_comparator", "in": "query" }, @@ -3977,7 +3977,7 @@ }, { "type": "string", - "description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`.", + "description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`.", "name": "sort_by", "in": "query" }, @@ -3989,7 +3989,7 @@ }, { "type": "string", - "description": "The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", + "description": "The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.", "name": "filter_by", "in": "query" }, @@ -4001,7 +4001,7 @@ }, { "type": "string", - "description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less` and `less_equals`. Defaults to `equals`", + "description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`", "name": "filter_comparator", "in": "query" }, @@ -4010,6 +4010,12 @@ "description": "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`.", "name": "filter_concat", "in": "query" + }, + { + "type": "string", + "description": "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`.", + "name": "filter_include_nulls", + "in": "query" } ], "responses": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 87300dd6615..ab165aa2e1b 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -2443,7 +2443,7 @@ paths: in: query name: order_by type: string - - description: The name of the field to filter by. Allowed values are all task properties except `labels`, `assignees`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. + - description: The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. in: query name: filter_by type: string @@ -2451,7 +2451,7 @@ paths: in: query name: filter_value type: string - - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals` and `like`. Defaults to `equals` + - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals` in: query name: filter_comparator type: string @@ -4459,7 +4459,7 @@ paths: in: query name: s type: string - - description: The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`. + - description: The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`. in: query name: sort_by type: string @@ -4467,7 +4467,7 @@ paths: in: query name: order_by type: string - - description: The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. + - description: The name of the field to filter by. Allowed values are all task properties except `labels`, `list` and `namespace`. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. in: query name: filter_by type: string @@ -4475,7 +4475,7 @@ paths: in: query name: filter_value type: string - - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less` and `less_equals`. Defaults to `equals` + - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals` in: query name: filter_comparator type: string @@ -4483,6 +4483,10 @@ paths: in: query name: filter_concat type: string + - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. + in: query + name: filter_include_nulls + type: string produces: - application/json responses: