feat: initial memo store

This commit is contained in:
Steven 2023-12-10 16:57:01 +08:00
parent 5c3df55b72
commit add523f8a5
7 changed files with 637 additions and 0 deletions

View File

@ -14,6 +14,9 @@
- [store/collection.proto](#store_collection-proto) - [store/collection.proto](#store_collection-proto)
- [Collection](#slash-store-Collection) - [Collection](#slash-store-Collection)
- [store/memo.proto](#store_memo-proto)
- [Memo](#slash-store-Memo)
- [store/shortcut.proto](#store_shortcut-proto) - [store/shortcut.proto](#store_shortcut-proto)
- [OpenGraphMetadata](#slash-store-OpenGraphMetadata) - [OpenGraphMetadata](#slash-store-OpenGraphMetadata)
- [Shortcut](#slash-store-Shortcut) - [Shortcut](#slash-store-Shortcut)
@ -168,6 +171,46 @@
<a name="store_memo-proto"></a>
<p align="right"><a href="#top">Top</a></p>
## store/memo.proto
<a name="slash-store-Memo"></a>
### Memo
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |
| creator_id | [int32](#int32) | | |
| created_ts | [int64](#int64) | | |
| updated_ts | [int64](#int64) | | |
| row_status | [RowStatus](#slash-store-RowStatus) | | |
| name | [string](#string) | | |
| title | [string](#string) | | |
| content | [string](#string) | | |
| tags | [string](#string) | repeated | |
| visibility | [Visibility](#slash-store-Visibility) | | |
<a name="store_shortcut-proto"></a> <a name="store_shortcut-proto"></a>
<p align="right"><a href="#top">Top</a></p> <p align="right"><a href="#top">Top</a></p>

247
proto/gen/store/memo.pb.go Normal file
View File

@ -0,0 +1,247 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: store/memo.proto
package store
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Memo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatorId int32 `protobuf:"varint,2,opt,name=creator_id,json=creatorId,proto3" json:"creator_id,omitempty"`
CreatedTs int64 `protobuf:"varint,3,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
UpdatedTs int64 `protobuf:"varint,4,opt,name=updated_ts,json=updatedTs,proto3" json:"updated_ts,omitempty"`
RowStatus RowStatus `protobuf:"varint,5,opt,name=row_status,json=rowStatus,proto3,enum=slash.store.RowStatus" json:"row_status,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Title string `protobuf:"bytes,7,opt,name=title,proto3" json:"title,omitempty"`
Content string `protobuf:"bytes,8,opt,name=content,proto3" json:"content,omitempty"`
Tags []string `protobuf:"bytes,9,rep,name=tags,proto3" json:"tags,omitempty"`
Visibility Visibility `protobuf:"varint,10,opt,name=visibility,proto3,enum=slash.store.Visibility" json:"visibility,omitempty"`
}
func (x *Memo) Reset() {
*x = Memo{}
if protoimpl.UnsafeEnabled {
mi := &file_store_memo_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Memo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Memo) ProtoMessage() {}
func (x *Memo) ProtoReflect() protoreflect.Message {
mi := &file_store_memo_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Memo.ProtoReflect.Descriptor instead.
func (*Memo) Descriptor() ([]byte, []int) {
return file_store_memo_proto_rawDescGZIP(), []int{0}
}
func (x *Memo) GetId() int32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Memo) GetCreatorId() int32 {
if x != nil {
return x.CreatorId
}
return 0
}
func (x *Memo) GetCreatedTs() int64 {
if x != nil {
return x.CreatedTs
}
return 0
}
func (x *Memo) GetUpdatedTs() int64 {
if x != nil {
return x.UpdatedTs
}
return 0
}
func (x *Memo) GetRowStatus() RowStatus {
if x != nil {
return x.RowStatus
}
return RowStatus_ROW_STATUS_UNSPECIFIED
}
func (x *Memo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Memo) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *Memo) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
func (x *Memo) GetTags() []string {
if x != nil {
return x.Tags
}
return nil
}
func (x *Memo) GetVisibility() Visibility {
if x != nil {
return x.Visibility
}
return Visibility_VISIBILITY_UNSPECIFIED
}
var File_store_memo_proto protoreflect.FileDescriptor
var file_store_memo_proto_rawDesc = []byte{
0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x0b, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x1a,
0x12, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x02, 0x0a, 0x04, 0x4d, 0x65, 0x6d, 0x6f, 0x12, 0x0e, 0x0a, 0x02,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a,
0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05,
0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70,
0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x73, 0x12, 0x35, 0x0a, 0x0a, 0x72, 0x6f, 0x77,
0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e,
0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x6f, 0x77, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x09, 0x72, 0x6f, 0x77, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x07, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e,
0x74, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x09, 0x20, 0x03,
0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x76, 0x69, 0x73, 0x69,
0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x73,
0x6c, 0x61, 0x73, 0x68, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x69, 0x73, 0x69, 0x62,
0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74,
0x79, 0x42, 0x9a, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x42, 0x09, 0x4d, 0x65, 0x6d, 0x6f, 0x50, 0x72, 0x6f, 0x74, 0x6f,
0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79,
0x6f, 0x75, 0x72, 0x73, 0x65, 0x6c, 0x66, 0x68, 0x6f, 0x73, 0x74, 0x65, 0x64, 0x2f, 0x73, 0x6c,
0x61, 0x73, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x74,
0x6f, 0x72, 0x65, 0xa2, 0x02, 0x03, 0x53, 0x53, 0x58, 0xaa, 0x02, 0x0b, 0x53, 0x6c, 0x61, 0x73,
0x68, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0xca, 0x02, 0x0b, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c,
0x53, 0x74, 0x6f, 0x72, 0x65, 0xe2, 0x02, 0x17, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c, 0x53, 0x74,
0x6f, 0x72, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea,
0x02, 0x0c, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x3a, 0x3a, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_store_memo_proto_rawDescOnce sync.Once
file_store_memo_proto_rawDescData = file_store_memo_proto_rawDesc
)
func file_store_memo_proto_rawDescGZIP() []byte {
file_store_memo_proto_rawDescOnce.Do(func() {
file_store_memo_proto_rawDescData = protoimpl.X.CompressGZIP(file_store_memo_proto_rawDescData)
})
return file_store_memo_proto_rawDescData
}
var file_store_memo_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_store_memo_proto_goTypes = []interface{}{
(*Memo)(nil), // 0: slash.store.Memo
(RowStatus)(0), // 1: slash.store.RowStatus
(Visibility)(0), // 2: slash.store.Visibility
}
var file_store_memo_proto_depIdxs = []int32{
1, // 0: slash.store.Memo.row_status:type_name -> slash.store.RowStatus
2, // 1: slash.store.Memo.visibility:type_name -> slash.store.Visibility
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_store_memo_proto_init() }
func file_store_memo_proto_init() {
if File_store_memo_proto != nil {
return
}
file_store_common_proto_init()
if !protoimpl.UnsafeEnabled {
file_store_memo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Memo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_store_memo_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_store_memo_proto_goTypes,
DependencyIndexes: file_store_memo_proto_depIdxs,
MessageInfos: file_store_memo_proto_msgTypes,
}.Build()
File_store_memo_proto = out.File
file_store_memo_proto_rawDesc = nil
file_store_memo_proto_goTypes = nil
file_store_memo_proto_depIdxs = nil
}

29
proto/store/memo.proto Normal file
View File

@ -0,0 +1,29 @@
syntax = "proto3";
package slash.store;
import "store/common.proto";
option go_package = "gen/store";
message Memo {
int32 id = 1;
int32 creator_id = 2;
int64 created_ts = 3;
int64 updated_ts = 4;
RowStatus row_status = 5;
string name = 6;
string title = 7;
string content = 8;
repeated string tags = 9;
Visibility visibility = 10;
}

View File

@ -74,3 +74,19 @@ CREATE TABLE collection (
); );
CREATE INDEX idx_collection_name ON collection(name); CREATE INDEX idx_collection_name ON collection(name);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
name TEXT NOT NULL UNIQUE,
title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
tag TEXT NOT NULL DEFAULT ''
);
CREATE INDEX idx_memo_name ON memo(name);

238
store/memo.go Normal file
View File

@ -0,0 +1,238 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
storepb "github.com/yourselfhosted/slash/proto/gen/store"
)
type UpdateMemo struct {
ID int32
RowStatus *RowStatus
Name *string
Title *string
Content *string
Visibility *Visibility
Tag *string
}
type FindMemo struct {
ID *int32
CreatorID *int32
RowStatus *RowStatus
Name *string
VisibilityList []Visibility
Tag *string
}
type DeleteMemo struct {
ID int32
}
func (s *Store) CreateMemo(ctx context.Context, create *storepb.Memo) (*storepb.Memo, error) {
set := []string{"creator_id", "name", "title", "content", "visibility", "tag"}
args := []any{create.CreatorId, create.Name, create.Title, create.Content, create.Visibility.String(), strings.Join(create.Tags, " ")}
stmt := `
INSERT INTO memo (
` + strings.Join(set, ", ") + `
)
VALUES (` + placeholders(len(args)) + `)
RETURNING id, created_ts, updated_ts, row_status
`
var rowStatus string
if err := s.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.Id,
&create.CreatedTs,
&create.UpdatedTs,
&rowStatus,
); err != nil {
return nil, err
}
create.RowStatus = convertRowStatusStringToStorepb(rowStatus)
memo := create
return memo, nil
}
func (s *Store) UpdateMemo(ctx context.Context, update *UpdateMemo) (*storepb.Memo, error) {
set, args := []string{}, []any{}
if update.RowStatus != nil {
set, args = append(set, "row_status = ?"), append(args, update.RowStatus.String())
}
if update.Name != nil {
set, args = append(set, "name = ?"), append(args, *update.Name)
}
if update.Title != nil {
set, args = append(set, "title = ?"), append(args, *update.Title)
}
if update.Content != nil {
set, args = append(set, "content = ?"), append(args, *update.Content)
}
if update.Visibility != nil {
set, args = append(set, "visibility = ?"), append(args, update.Visibility.String())
}
if update.Tag != nil {
set, args = append(set, "tag = ?"), append(args, *update.Tag)
}
if len(set) == 0 {
return nil, errors.New("no update specified")
}
args = append(args, update.ID)
stmt := `
UPDATE memo
SET
` + strings.Join(set, ", ") + `
WHERE
id = ?
RETURNING id, creator_id, created_ts, updated_ts, row_status, name, title, content, visibility, tag
`
memo := &storepb.Memo{}
var rowStatus, visibility, tags string
if err := s.db.QueryRowContext(ctx, stmt, args...).Scan(
&memo.Id,
&memo.CreatorId,
&memo.CreatedTs,
&memo.UpdatedTs,
&rowStatus,
&memo.Name,
&memo.Title,
&memo.Content,
&visibility,
&tags,
); err != nil {
return nil, err
}
memo.RowStatus = convertRowStatusStringToStorepb(rowStatus)
memo.Visibility = convertVisibilityStringToStorepb(visibility)
memo.Tags = filterTags(strings.Split(tags, " "))
return memo, nil
}
func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*storepb.Memo, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.CreatorID; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
}
if v := find.VisibilityList; len(v) != 0 {
list := []string{}
for _, visibility := range v {
list = append(list, fmt.Sprintf("$%d", len(args)+1))
args = append(args, visibility)
}
where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ",")))
}
if v := find.Tag; v != nil {
where, args = append(where, "tag LIKE ?"), append(args, "%"+*v+"%")
}
rows, err := s.db.QueryContext(ctx, `
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
name,
title,
content,
visibility,
tag
FROM memo
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]*storepb.Memo, 0)
for rows.Next() {
memo := &storepb.Memo{}
var rowStatus, visibility, tags string
if err := rows.Scan(
&memo.Id,
&memo.CreatorId,
&memo.CreatedTs,
&memo.UpdatedTs,
&rowStatus,
&memo.Name,
&memo.Title,
&memo.Content,
&visibility,
&tags,
); err != nil {
return nil, err
}
memo.RowStatus = convertRowStatusStringToStorepb(rowStatus)
memo.Visibility = storepb.Visibility(storepb.Visibility_value[visibility])
memo.Tags = filterTags(strings.Split(tags, " "))
list = append(list, memo)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (s *Store) GetMemo(ctx context.Context, find *FindMemo) (*storepb.Memo, error) {
memos, err := s.ListMemos(ctx, find)
if err != nil {
return nil, err
}
if len(memos) == 0 {
return nil, nil
}
memo := memos[0]
return memo, nil
}
func (s *Store) DeleteMemo(ctx context.Context, delete *DeleteMemo) error {
if _, err := s.db.ExecContext(ctx, `DELETE FROM memo WHERE id = ?`, delete.ID); err != nil {
return err
}
return nil
}
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return err
}
return nil
}
func placeholders(n int) string {
return strings.Repeat("?,", n-1) + "?"
}

View File

@ -238,6 +238,10 @@ func (s *Store) DeleteUser(ctx context.Context, delete *DeleteUser) error {
return err return err
} }
if err := vacuumMemo(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return err return err
} }

60
test/store/memo_test.go Normal file
View File

@ -0,0 +1,60 @@
package teststore
import (
"context"
"testing"
"github.com/stretchr/testify/require"
storepb "github.com/yourselfhosted/slash/proto/gen/store"
"github.com/yourselfhosted/slash/store"
)
func TestMemoStore(t *testing.T) {
ctx := context.Background()
ts := NewTestingStore(ctx, t)
user, err := createTestingAdminUser(ctx, ts)
require.NoError(t, err)
memo, err := ts.CreateMemo(ctx, &storepb.Memo{
CreatorId: user.ID,
Name: "test",
Title: "Test Memo",
Content: "This is a test memo.",
Visibility: storepb.Visibility_PRIVATE,
Tags: []string{"test", "memo"},
})
require.NoError(t, err)
memos, err := ts.ListMemos(ctx, &store.FindMemo{
CreatorID: &user.ID,
})
require.NoError(t, err)
require.Equal(t, 1, len(memos))
require.Equal(t, memo, memos[0])
newContent := "Updated content."
updatedMemo, err := ts.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.Id,
Content: &newContent,
})
require.NoError(t, err)
require.Equal(t, newContent, updatedMemo.Content)
tag := "test"
memo, err = ts.GetMemo(ctx, &store.FindMemo{
Tag: &tag,
})
require.NoError(t, err)
err = ts.DeleteMemo(ctx, &store.DeleteMemo{
ID: memo.Id,
})
require.NoError(t, err)
memos, err = ts.ListMemos(ctx, &store.FindMemo{
CreatorID: &user.ID,
})
require.NoError(t, err)
require.Equal(t, 0, len(memos))
}