Skip to content

Commit 78aa413

Browse files
committed
feat: implement attachment filtering functionality
1 parent 955ff0c commit 78aa413

File tree

12 files changed

+210
-15
lines changed

12 files changed

+210
-15
lines changed

plugin/filter/engine.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ func (p *Program) Render(opts RenderOptions) (Statement, error) {
9696
}
9797

9898
var (
99-
defaultOnce sync.Once
100-
defaultInst *Engine
101-
defaultErr error
99+
defaultOnce sync.Once
100+
defaultInst *Engine
101+
defaultErr error
102+
defaultAttachmentOnce sync.Once
103+
defaultAttachmentInst *Engine
104+
defaultAttachmentErr error
102105
)
103106

104107
// DefaultEngine returns the process-wide memo filter engine.
@@ -109,6 +112,14 @@ func DefaultEngine() (*Engine, error) {
109112
return defaultInst, defaultErr
110113
}
111114

115+
// DefaultAttachmentEngine returns the process-wide attachment filter engine.
116+
func DefaultAttachmentEngine() (*Engine, error) {
117+
defaultAttachmentOnce.Do(func() {
118+
defaultAttachmentInst, defaultAttachmentErr = NewEngine(NewAttachmentSchema())
119+
})
120+
return defaultAttachmentInst, defaultAttachmentErr
121+
}
122+
112123
func normalizeLegacyFilter(expr string) string {
113124
expr = rewriteNumericLogicalOperand(expr, "&&")
114125
expr = rewriteNumericLogicalOperand(expr, "||")

plugin/filter/schema.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,62 @@ func NewSchema() Schema {
243243
}
244244
}
245245

246+
// NewAttachmentSchema constructs the attachment filter schema and CEL environment.
247+
func NewAttachmentSchema() Schema {
248+
fields := map[string]Field{
249+
"filename": {
250+
Name: "filename",
251+
Kind: FieldKindScalar,
252+
Type: FieldTypeString,
253+
Column: Column{Table: "resource", Name: "filename"},
254+
SupportsContains: true,
255+
Expressions: map[DialectName]string{},
256+
},
257+
"mime_type": {
258+
Name: "mime_type",
259+
Kind: FieldKindScalar,
260+
Type: FieldTypeString,
261+
Column: Column{Table: "resource", Name: "type"},
262+
Expressions: map[DialectName]string{},
263+
},
264+
"create_time": {
265+
Name: "create_time",
266+
Kind: FieldKindScalar,
267+
Type: FieldTypeTimestamp,
268+
Column: Column{Table: "resource", Name: "created_ts"},
269+
Expressions: map[DialectName]string{
270+
DialectMySQL: "UNIX_TIMESTAMP(%s)",
271+
DialectPostgres: "EXTRACT(EPOCH FROM TO_TIMESTAMP(%s))",
272+
},
273+
},
274+
"memo": {
275+
Name: "memo",
276+
Kind: FieldKindScalar,
277+
Type: FieldTypeString,
278+
Column: Column{Table: "resource", Name: "memo_uid"},
279+
Expressions: map[DialectName]string{},
280+
AllowedComparisonOps: map[ComparisonOperator]bool{
281+
CompareEq: true,
282+
CompareNeq: true,
283+
},
284+
},
285+
}
286+
287+
envOptions := []cel.EnvOption{
288+
cel.Variable("filename", cel.StringType),
289+
cel.Variable("mime_type", cel.StringType),
290+
cel.Variable("create_time", cel.IntType),
291+
cel.Variable("memo", cel.StringType),
292+
nowFunction,
293+
}
294+
295+
return Schema{
296+
Name: "attachment",
297+
Fields: fields,
298+
EnvOptions: envOptions,
299+
}
300+
}
301+
246302
// columnExpr returns the field expression for the given dialect, applying
247303
// any schema-specific overrides (e.g. UNIX timestamp conversions).
248304
func (f Field) columnExpr(d DialectName) string {

proto/api/v1/attachment_service.proto

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ message ListAttachmentsRequest {
101101
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
102102

103103
// Optional. Filter to apply to the list results.
104-
// Example: "type=image/png" or "filename:*.jpg"
105-
// Supported operators: =, !=, <, <=, >, >=, :
106-
// Supported fields: filename, type, size, create_time, memo
104+
// Example: "mime_type==\"image/png\"" or "filename.contains(\"test\")"
105+
// Supported operators: =, !=, <, <=, >, >=, : (contains), in
106+
// Supported fields: filename, mime_type, create_time, memo
107107
string filter = 3 [(google.api.field_behavior) = OPTIONAL];
108108

109109
// Optional. The order to sort results by.

proto/gen/api/v1/attachment_service.pb.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/gen/openapi.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ paths:
9797
in: query
9898
description: |-
9999
Optional. Filter to apply to the list results.
100-
Example: "type=image/png" or "filename:*.jpg"
101-
Supported operators: =, !=, <, <=, >, >=, :
102-
Supported fields: filename, type, size, create_time, memo
100+
Example: "mime_type==\"image/png\"" or "filename.contains(\"test\")"
101+
Supported operators: =, !=, <, <=, >, >=, : (contains), in
102+
Supported fields: filename, mime_type, create_time, memo
103103
schema:
104104
type: string
105105
- name: orderBy

server/router/api/v1/attachment_service.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/usememos/memos/internal/profile"
2323
"github.com/usememos/memos/internal/util"
24+
"github.com/usememos/memos/plugin/filter"
2425
"github.com/usememos/memos/plugin/storage/s3"
2526
v1pb "github.com/usememos/memos/proto/gen/api/v1"
2627
storepb "github.com/usememos/memos/proto/gen/store"
@@ -156,6 +157,14 @@ func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAt
156157
Offset: &offset,
157158
}
158159

160+
// Parse filter if provided
161+
if request.Filter != "" {
162+
if err := s.validateAttachmentFilter(ctx, request.Filter); err != nil {
163+
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
164+
}
165+
findAttachment.Filters = append(findAttachment.Filters, request.Filter)
166+
}
167+
159168
attachments, err := s.Store.ListAttachments(ctx, findAttachment)
160169
if err != nil {
161170
return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err)
@@ -472,3 +481,29 @@ func isValidMimeType(mimeType string) bool {
472481
matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$`, mimeType)
473482
return matched
474483
}
484+
485+
func (s *APIV1Service) validateAttachmentFilter(ctx context.Context, filterStr string) error {
486+
if filterStr == "" {
487+
return errors.New("filter cannot be empty")
488+
}
489+
490+
engine, err := filter.DefaultAttachmentEngine()
491+
if err != nil {
492+
return err
493+
}
494+
495+
var dialect filter.DialectName
496+
switch s.Profile.Driver {
497+
case "mysql":
498+
dialect = filter.DialectMySQL
499+
case "postgres":
500+
dialect = filter.DialectPostgres
501+
default:
502+
dialect = filter.DialectSQLite
503+
}
504+
505+
if _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: dialect}); err != nil {
506+
return errors.Wrap(err, "failed to compile filter")
507+
}
508+
return nil
509+
}

store/attachment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type FindAttachment struct {
5151
MemoIDList []int32
5252
HasRelatedMemo bool
5353
StorageType *storepb.AttachmentStorageType
54+
Filters []string
5455
Limit *int
5556
Offset *int
5657
}

store/db/mysql/attachment.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pkg/errors"
1010
"google.golang.org/protobuf/encoding/protojson"
1111

12+
"github.com/usememos/memos/plugin/filter"
1213
storepb "github.com/usememos/memos/proto/gen/store"
1314
"github.com/usememos/memos/store"
1415
)
@@ -83,6 +84,16 @@ func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([
8384
where, args = append(where, "`resource`.`storage_type` = ?"), append(args, find.StorageType.String())
8485
}
8586

87+
if len(find.Filters) > 0 {
88+
engine, err := filter.DefaultAttachmentEngine()
89+
if err != nil {
90+
return nil, errors.Wrap(err, "failed to get filter engine")
91+
}
92+
if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectMySQL, &where, &args); err != nil {
93+
return nil, errors.Wrap(err, "failed to append filter conditions")
94+
}
95+
}
96+
8697
fields := []string{
8798
"`resource`.`id` AS `id`",
8899
"`resource`.`uid` AS `uid`",

store/db/postgres/attachment.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pkg/errors"
1010
"google.golang.org/protobuf/encoding/protojson"
1111

12+
"github.com/usememos/memos/plugin/filter"
1213
storepb "github.com/usememos/memos/proto/gen/store"
1314
"github.com/usememos/memos/store"
1415
)
@@ -72,6 +73,16 @@ func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([
7273
where, args = append(where, "resource.storage_type = "+placeholder(len(args)+1)), append(args, v.String())
7374
}
7475

76+
if len(find.Filters) > 0 {
77+
engine, err := filter.DefaultAttachmentEngine()
78+
if err != nil {
79+
return nil, errors.Wrap(err, "failed to get filter engine")
80+
}
81+
if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectPostgres, &where, &args); err != nil {
82+
return nil, errors.Wrap(err, "failed to append filter conditions")
83+
}
84+
}
85+
7586
fields := []string{
7687
"resource.id AS id",
7788
"resource.uid AS uid",

store/db/sqlite/attachment.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pkg/errors"
1010
"google.golang.org/protobuf/encoding/protojson"
1111

12+
"github.com/usememos/memos/plugin/filter"
1213
storepb "github.com/usememos/memos/proto/gen/store"
1314
"github.com/usememos/memos/store"
1415
)
@@ -76,6 +77,16 @@ func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([
7677
where, args = append(where, "`resource`.`storage_type` = ?"), append(args, find.StorageType.String())
7778
}
7879

80+
if len(find.Filters) > 0 {
81+
engine, err := filter.DefaultAttachmentEngine()
82+
if err != nil {
83+
return nil, errors.Wrap(err, "failed to get filter engine")
84+
}
85+
if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil {
86+
return nil, errors.Wrap(err, "failed to append filter conditions")
87+
}
88+
}
89+
7990
fields := []string{
8091
"`resource`.`id` AS `id`",
8192
"`resource`.`uid` AS `uid`",

0 commit comments

Comments
 (0)