mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement hasTaskList filter
This commit is contained in:
@@ -17,6 +17,7 @@ var MemoFilterCELAttributes = []cel.EnvOption{
|
|||||||
cel.Variable("tag", cel.StringType),
|
cel.Variable("tag", cel.StringType),
|
||||||
cel.Variable("update_time", cel.StringType),
|
cel.Variable("update_time", cel.StringType),
|
||||||
cel.Variable("visibility", cel.StringType),
|
cel.Variable("visibility", cel.StringType),
|
||||||
|
cel.Variable("has_task_list", cel.BoolType),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the filter string and returns the parsed expression.
|
// Parse parses the filter string and returns the parsed expression.
|
||||||
|
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content"}, identifier) {
|
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||||
}
|
}
|
||||||
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
||||||
@@ -138,6 +138,25 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Args = append(ctx.Args, valueInt)
|
ctx.Args = append(ctx.Args, valueInt)
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
if operator != "=" && operator != "!=" {
|
||||||
|
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||||
|
}
|
||||||
|
valueBool, ok := value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid boolean value for has_task_list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In MySQL, we can use JSON_EXTRACT to get the value and compare it to 'true' or 'false'
|
||||||
|
compareValue := "false"
|
||||||
|
if valueBool {
|
||||||
|
compareValue = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL uses -> as a shorthand for JSON_EXTRACT
|
||||||
|
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s CAST('%s' AS JSON)", operator, compareValue)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "@in":
|
case "@in":
|
||||||
if len(v.CallExpr.Args) != 2 {
|
if len(v.CallExpr.Args) != 2 {
|
||||||
@@ -207,13 +226,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
}
|
}
|
||||||
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
||||||
identifier := v.IdentExpr.GetName()
|
identifier := v.IdentExpr.GetName()
|
||||||
if !slices.Contains([]string{"pinned"}, identifier) {
|
if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier for %s", identifier)
|
return errors.Errorf("invalid identifier for %s", identifier)
|
||||||
}
|
}
|
||||||
if identifier == "pinned" {
|
if identifier == "pinned" {
|
||||||
if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil {
|
if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@@ -59,6 +59,41 @@ func TestConvertExprToSQL(t *testing.T) {
|
|||||||
want: "`memo`.`pinned` IS TRUE",
|
want: "`memo`.`pinned` IS TRUE",
|
||||||
args: []any{},
|
args: []any{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == true`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list != false`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != CAST('false' AS JSON)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == false`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('false' AS JSON)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `!has_task_list`,
|
||||||
|
want: "NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON))",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && pinned`,
|
||||||
|
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`pinned` IS TRUE)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && content.contains("todo")`,
|
||||||
|
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`content` LIKE ?)",
|
||||||
|
args: []any{"%todo%"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content"}, identifier) {
|
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||||
}
|
}
|
||||||
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
||||||
@@ -135,6 +135,20 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Args = append(ctx.Args, valueInt)
|
ctx.Args = append(ctx.Args, valueInt)
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
if operator != "=" && operator != "!=" {
|
||||||
|
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||||
|
}
|
||||||
|
valueBool, ok := value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid boolean value for has_task_list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In PostgreSQL, extract the boolean from the JSON and compare it
|
||||||
|
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(memo.payload->'property'->>'hasTaskList')::boolean %s %s", operator, placeholder(len(ctx.Args)+ctx.ArgsOffset+1))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.Args = append(ctx.Args, valueBool)
|
||||||
}
|
}
|
||||||
case "@in":
|
case "@in":
|
||||||
if len(v.CallExpr.Args) != 2 {
|
if len(v.CallExpr.Args) != 2 {
|
||||||
@@ -204,13 +218,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
}
|
}
|
||||||
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
||||||
identifier := v.IdentExpr.GetName()
|
identifier := v.IdentExpr.GetName()
|
||||||
if !slices.Contains([]string{"pinned"}, identifier) {
|
if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier %s", identifier)
|
return errors.Errorf("invalid identifier %s", identifier)
|
||||||
}
|
}
|
||||||
if identifier == "pinned" {
|
if identifier == "pinned" {
|
||||||
if _, err := ctx.Buffer.WriteString("memo.pinned IS TRUE"); err != nil {
|
if _, err := ctx.Buffer.WriteString("memo.pinned IS TRUE"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
if _, err := ctx.Buffer.WriteString("(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@@ -59,6 +59,41 @@ func TestRestoreExprToSQL(t *testing.T) {
|
|||||||
want: "memo.pinned IS TRUE",
|
want: "memo.pinned IS TRUE",
|
||||||
args: []any{},
|
args: []any{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list`,
|
||||||
|
want: "(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == true`,
|
||||||
|
want: "(memo.payload->'property'->>'hasTaskList')::boolean = $1",
|
||||||
|
args: []any{true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list != false`,
|
||||||
|
want: "(memo.payload->'property'->>'hasTaskList')::boolean != $1",
|
||||||
|
args: []any{false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == false`,
|
||||||
|
want: "(memo.payload->'property'->>'hasTaskList')::boolean = $1",
|
||||||
|
args: []any{false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `!has_task_list`,
|
||||||
|
want: "NOT ((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && pinned`,
|
||||||
|
want: "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.pinned IS TRUE)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && content.contains("todo")`,
|
||||||
|
want: "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.content ILIKE $1)",
|
||||||
|
args: []any{"%todo%"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content"}, identifier) {
|
if !slices.Contains([]string{"creator_id", "create_time", "update_time", "visibility", "content", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||||
}
|
}
|
||||||
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
value, err := filter.GetConstValue(v.CallExpr.Args[1])
|
||||||
@@ -138,6 +138,22 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Args = append(ctx.Args, valueInt)
|
ctx.Args = append(ctx.Args, valueInt)
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
if operator != "=" && operator != "!=" {
|
||||||
|
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||||
|
}
|
||||||
|
valueBool, ok := value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid boolean value for has_task_list")
|
||||||
|
}
|
||||||
|
// In SQLite JSON boolean values are 1 for true and 0 for false
|
||||||
|
compareValue := 0
|
||||||
|
if valueBool {
|
||||||
|
compareValue = 1
|
||||||
|
}
|
||||||
|
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s %d", operator, compareValue)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "@in":
|
case "@in":
|
||||||
if len(v.CallExpr.Args) != 2 {
|
if len(v.CallExpr.Args) != 2 {
|
||||||
@@ -207,13 +223,18 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
|
|||||||
}
|
}
|
||||||
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
||||||
identifier := v.IdentExpr.GetName()
|
identifier := v.IdentExpr.GetName()
|
||||||
if !slices.Contains([]string{"pinned"}, identifier) {
|
if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) {
|
||||||
return errors.Errorf("invalid identifier %s", identifier)
|
return errors.Errorf("invalid identifier %s", identifier)
|
||||||
}
|
}
|
||||||
if identifier == "pinned" {
|
if identifier == "pinned" {
|
||||||
if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil {
|
if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if identifier == "has_task_list" {
|
||||||
|
// Handle has_task_list as a standalone boolean identifier
|
||||||
|
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@@ -74,6 +74,41 @@ func TestConvertExprToSQL(t *testing.T) {
|
|||||||
want: "(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))",
|
want: "(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))",
|
||||||
args: []any{int64(101), "PUBLIC", "PRIVATE"},
|
args: []any{int64(101), "PUBLIC", "PRIVATE"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == true`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list != false`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 0",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list == false`,
|
||||||
|
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 0",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `!has_task_list`,
|
||||||
|
want: "NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && pinned`,
|
||||||
|
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`pinned` IS TRUE)",
|
||||||
|
args: []any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: `has_task_list && content.contains("todo")`,
|
||||||
|
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`content` LIKE ?)",
|
||||||
|
args: []any{"%todo%"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
Reference in New Issue
Block a user