mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
255 lines
6.7 KiB
Go
255 lines
6.7 KiB
Go
package filter
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/google/cel-go/common/types/ref"
|
|
)
|
|
|
|
// DialectName enumerates supported SQL dialects.
|
|
type DialectName string
|
|
|
|
const (
|
|
DialectSQLite DialectName = "sqlite"
|
|
DialectMySQL DialectName = "mysql"
|
|
DialectPostgres DialectName = "postgres"
|
|
)
|
|
|
|
// FieldType represents the logical type of a field.
|
|
type FieldType string
|
|
|
|
const (
|
|
FieldTypeString FieldType = "string"
|
|
FieldTypeInt FieldType = "int"
|
|
FieldTypeBool FieldType = "bool"
|
|
FieldTypeTimestamp FieldType = "timestamp"
|
|
)
|
|
|
|
// FieldKind describes how a field is stored.
|
|
type FieldKind string
|
|
|
|
const (
|
|
FieldKindScalar FieldKind = "scalar"
|
|
FieldKindBoolColumn FieldKind = "bool_column"
|
|
FieldKindJSONBool FieldKind = "json_bool"
|
|
FieldKindJSONList FieldKind = "json_list"
|
|
FieldKindVirtualAlias FieldKind = "virtual_alias"
|
|
)
|
|
|
|
// Column identifies the backing table column.
|
|
type Column struct {
|
|
Table string
|
|
Name string
|
|
}
|
|
|
|
// Field captures the schema metadata for an exposed CEL identifier.
|
|
type Field struct {
|
|
Name string
|
|
Kind FieldKind
|
|
Type FieldType
|
|
Column Column
|
|
JSONPath []string
|
|
AliasFor string
|
|
SupportsContains bool
|
|
Expressions map[DialectName]string
|
|
AllowedComparisonOps map[ComparisonOperator]bool
|
|
}
|
|
|
|
// Schema collects CEL environment options and field metadata.
|
|
type Schema struct {
|
|
Name string
|
|
Fields map[string]Field
|
|
EnvOptions []cel.EnvOption
|
|
}
|
|
|
|
// Field returns the field metadata if present.
|
|
func (s Schema) Field(name string) (Field, bool) {
|
|
f, ok := s.Fields[name]
|
|
return f, ok
|
|
}
|
|
|
|
// ResolveAlias resolves a virtual alias to its target field.
|
|
func (s Schema) ResolveAlias(name string) (Field, bool) {
|
|
field, ok := s.Fields[name]
|
|
if !ok {
|
|
return Field{}, false
|
|
}
|
|
if field.Kind == FieldKindVirtualAlias {
|
|
target, ok := s.Fields[field.AliasFor]
|
|
if !ok {
|
|
return Field{}, false
|
|
}
|
|
return target, true
|
|
}
|
|
return field, true
|
|
}
|
|
|
|
var nowFunction = cel.Function("now",
|
|
cel.Overload("now",
|
|
[]*cel.Type{},
|
|
cel.IntType,
|
|
cel.FunctionBinding(func(_ ...ref.Val) ref.Val {
|
|
return types.Int(time.Now().Unix())
|
|
}),
|
|
),
|
|
)
|
|
|
|
// NewSchema constructs the memo filter schema and CEL environment.
|
|
func NewSchema() Schema {
|
|
fields := map[string]Field{
|
|
"content": {
|
|
Name: "content",
|
|
Kind: FieldKindScalar,
|
|
Type: FieldTypeString,
|
|
Column: Column{Table: "memo", Name: "content"},
|
|
SupportsContains: true,
|
|
Expressions: map[DialectName]string{},
|
|
},
|
|
"creator_id": {
|
|
Name: "creator_id",
|
|
Kind: FieldKindScalar,
|
|
Type: FieldTypeInt,
|
|
Column: Column{Table: "memo", Name: "creator_id"},
|
|
Expressions: map[DialectName]string{},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"created_ts": {
|
|
Name: "created_ts",
|
|
Kind: FieldKindScalar,
|
|
Type: FieldTypeTimestamp,
|
|
Column: Column{Table: "memo", Name: "created_ts"},
|
|
Expressions: map[DialectName]string{
|
|
DialectMySQL: "UNIX_TIMESTAMP(%s)",
|
|
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
|
|
},
|
|
},
|
|
"updated_ts": {
|
|
Name: "updated_ts",
|
|
Kind: FieldKindScalar,
|
|
Type: FieldTypeTimestamp,
|
|
Column: Column{Table: "memo", Name: "updated_ts"},
|
|
Expressions: map[DialectName]string{
|
|
DialectMySQL: "UNIX_TIMESTAMP(%s)",
|
|
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
|
|
},
|
|
},
|
|
"pinned": {
|
|
Name: "pinned",
|
|
Kind: FieldKindBoolColumn,
|
|
Type: FieldTypeBool,
|
|
Column: Column{Table: "memo", Name: "pinned"},
|
|
Expressions: map[DialectName]string{},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"visibility": {
|
|
Name: "visibility",
|
|
Kind: FieldKindScalar,
|
|
Type: FieldTypeString,
|
|
Column: Column{Table: "memo", Name: "visibility"},
|
|
Expressions: map[DialectName]string{},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"tags": {
|
|
Name: "tags",
|
|
Kind: FieldKindJSONList,
|
|
Type: FieldTypeString,
|
|
Column: Column{Table: "memo", Name: "payload"},
|
|
JSONPath: []string{"tags"},
|
|
},
|
|
"tag": {
|
|
Name: "tag",
|
|
Kind: FieldKindVirtualAlias,
|
|
Type: FieldTypeString,
|
|
AliasFor: "tags",
|
|
},
|
|
"has_task_list": {
|
|
Name: "has_task_list",
|
|
Kind: FieldKindJSONBool,
|
|
Type: FieldTypeBool,
|
|
Column: Column{Table: "memo", Name: "payload"},
|
|
JSONPath: []string{"property", "hasTaskList"},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"has_link": {
|
|
Name: "has_link",
|
|
Kind: FieldKindJSONBool,
|
|
Type: FieldTypeBool,
|
|
Column: Column{Table: "memo", Name: "payload"},
|
|
JSONPath: []string{"property", "hasLink"},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"has_code": {
|
|
Name: "has_code",
|
|
Kind: FieldKindJSONBool,
|
|
Type: FieldTypeBool,
|
|
Column: Column{Table: "memo", Name: "payload"},
|
|
JSONPath: []string{"property", "hasCode"},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
"has_incomplete_tasks": {
|
|
Name: "has_incomplete_tasks",
|
|
Kind: FieldKindJSONBool,
|
|
Type: FieldTypeBool,
|
|
Column: Column{Table: "memo", Name: "payload"},
|
|
JSONPath: []string{"property", "hasIncompleteTasks"},
|
|
AllowedComparisonOps: map[ComparisonOperator]bool{
|
|
CompareEq: true,
|
|
CompareNeq: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
envOptions := []cel.EnvOption{
|
|
cel.Variable("content", cel.StringType),
|
|
cel.Variable("creator_id", cel.IntType),
|
|
cel.Variable("created_ts", cel.IntType),
|
|
cel.Variable("updated_ts", cel.IntType),
|
|
cel.Variable("pinned", cel.BoolType),
|
|
cel.Variable("tag", cel.StringType),
|
|
cel.Variable("tags", cel.ListType(cel.StringType)),
|
|
cel.Variable("visibility", cel.StringType),
|
|
cel.Variable("has_task_list", cel.BoolType),
|
|
cel.Variable("has_link", cel.BoolType),
|
|
cel.Variable("has_code", cel.BoolType),
|
|
cel.Variable("has_incomplete_tasks", cel.BoolType),
|
|
nowFunction,
|
|
}
|
|
|
|
return Schema{
|
|
Name: "memo",
|
|
Fields: fields,
|
|
EnvOptions: envOptions,
|
|
}
|
|
}
|
|
|
|
// columnExpr returns the field expression for the given dialect, applying
|
|
// any schema-specific overrides (e.g. UNIX timestamp conversions).
|
|
func (f Field) columnExpr(d DialectName) string {
|
|
base := qualifyColumn(d, f.Column)
|
|
if expr, ok := f.Expressions[d]; ok && expr != "" {
|
|
return fmt.Sprintf(expr, base)
|
|
}
|
|
return base
|
|
}
|