chore: retire memo resource relation table

This commit is contained in:
Steven
2023-09-27 00:40:16 +08:00
parent 4f10198ec0
commit 6007f48b7d
21 changed files with 126 additions and 567 deletions

View File

@@ -319,9 +319,9 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
} }
for _, resourceID := range createMemoRequest.ResourceIDList { for _, resourceID := range createMemoRequest.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{ if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
MemoID: memo.ID, ID: resourceID,
ResourceID: resourceID, MemoID: &memo.ID,
}); err != nil { }); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
} }
@@ -694,19 +694,18 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
if patchMemoRequest.ResourceIDList != nil { if patchMemoRequest.ResourceIDList != nil {
addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList) addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList)
for _, resourceID := range addedResourceIDList { for _, resourceID := range addedResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{ if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
MemoID: memo.ID, ID: resourceID,
ResourceID: resourceID, MemoID: &memo.ID,
}); err != nil { }); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
} }
} }
for _, resourceID := range removedResourceIDList { for _, resourceID := range removedResourceIDList {
if err := s.Store.DeleteMemoResource(ctx, &store.DeleteMemoResource{ if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
MemoID: &memo.ID, ID: resourceID,
ResourceID: &resourceID,
}); err != nil { }); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
} }
} }
} }

View File

@@ -1,179 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/common/util"
"github.com/usememos/memos/store"
)
type MemoResource struct {
MemoID int32 `json:"memoId"`
ResourceID int32 `json:"resourceId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
}
type UpsertMemoResourceRequest struct {
ResourceID int32 `json:"resourceId"`
UpdatedTs *int64 `json:"updatedTs"`
}
type MemoResourceFind struct {
MemoID *int32
ResourceID *int32
}
type MemoResourceDelete struct {
MemoID *int32
ResourceID *int32
}
func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) {
g.GET("/memo/:memoId/resource", s.GetMemoResourceList)
g.POST("/memo/:memoId/resource", s.BindMemoResource)
g.DELETE("/memo/:memoId/resource/:resourceId", s.UnbindMemoResource)
}
// GetMemoResourceList godoc
//
// @Summary Get resource list of a memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to fetch resource list from"
// @Success 200 {object} []Resource "Memo resource list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 500 {object} nil "Failed to fetch resource list"
// @Router /api/v1/memo/{memoId}/resource [GET]
func (s *APIV1Service) GetMemoResourceList(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
list, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
resourceList := []*Resource{}
for _, resource := range list {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceList)
}
// BindMemoResource godoc
//
// @Summary Bind resource to memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to bind resource to"
// @Param body body UpsertMemoResourceRequest true "Memo resource request object"
// @Success 200 {boolean} true "Memo resource binded"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo resource request | Resource not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized to bind this resource"
// @Failure 500 {object} nil "Failed to fetch resource | Failed to upsert memo resource"
// @Router /api/v1/memo/{memoId}/resource [POST]
//
// NOTES:
// - Passing 0 to updatedTs will set it to 0 in the database, which is probably unwanted.
func (s *APIV1Service) BindMemoResource(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
request := &UpsertMemoResourceRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &request.ResourceID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
} else if resource.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
}
upsert := &store.UpsertMemoResource{
MemoID: memoID,
ResourceID: request.ResourceID,
CreatedTs: time.Now().Unix(),
}
if request.UpdatedTs != nil {
upsert.UpdatedTs = request.UpdatedTs
}
if _, err := s.Store.UpsertMemoResource(ctx, upsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UnbindMemoResource godoc
//
// @Summary Unbind resource from memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to unbind resource from"
// @Param resourceId path int true "ID of resource to unbind from memo"
// @Success 200 {boolean} true "Memo resource unbinded. *200 is returned even if the reference doesn't exists "
// @Failure 400 {object} nil "Memo ID is not a number: %s | Resource ID is not a number: %s | Memo not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find memo | Failed to fetch resource list"
// @Router /api/v1/memo/{memoId}/resource/{resourceId} [DELETE]
func (s *APIV1Service) UnbindMemoResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
if memo == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Memo not found")
}
if memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
if err := s.Store.DeleteMemoResource(ctx, &store.DeleteMemoResource{
MemoID: &memoID,
ResourceID: &resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}

View File

@@ -384,17 +384,6 @@ func (s *APIV1Service) streamResource(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
} }
resourceVisibility, err := checkResourceVisibility(ctx, s.Store, resourceID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to get resource visibility").SetInternal(err)
}
// Protected resource require a logined user
userID, ok := c.Get(userIDContextKey).(int32)
if resourceVisibility == store.Protected && (!ok || userID <= 0) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
GetBlob: true, GetBlob: true,
@@ -405,10 +394,20 @@ func (s *APIV1Service) streamResource(c echo.Context) error {
if resource == nil { if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID)) return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
} }
// Check the related memo visibility.
// Private resource require logined user is the creator if resource.MemoID != nil {
if resourceVisibility == store.Private && (!ok || userID != resource.CreatorID) { memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err) ID: resource.MemoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", resource.MemoID)).SetInternal(err)
}
if memo != nil && memo.Visibility != store.Public {
userID, ok := c.Get(userIDContextKey).(int32)
if !ok || (memo.Visibility == store.Private && userID != resource.CreatorID) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match")
}
}
} }
blob := resource.Blob blob := resource.Blob
@@ -542,47 +541,6 @@ func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error)
return dstBlob, nil return dstBlob, nil
} }
func checkResourceVisibility(ctx context.Context, s *store.Store, resourceID int32) (store.Visibility, error) {
memoResources, err := s.ListMemoResources(ctx, &store.FindMemoResource{
ResourceID: &resourceID,
})
if err != nil {
return store.Private, err
}
// If resource is belongs to no memo, it'll always PRIVATE.
if len(memoResources) == 0 {
return store.Private, nil
}
memoIDs := make([]int32, 0, len(memoResources))
for _, memoResource := range memoResources {
memoIDs = append(memoIDs, memoResource.MemoID)
}
visibilityList, err := s.FindMemosVisibilityList(ctx, memoIDs)
if err != nil {
return store.Private, err
}
var isProtected bool
for _, visibility := range visibilityList {
// If any memo is PUBLIC, resource should be PUBLIC too.
if visibility == store.Public {
return store.Public, nil
}
if visibility == store.Protected {
isProtected = true
}
}
if isProtected {
return store.Protected, nil
}
return store.Private, nil
}
func convertResourceFromStore(resource *store.Resource) *Resource { func convertResourceFromStore(resource *store.Resource) *Resource {
return &Resource{ return &Resource{
ID: resource.ID, ID: resource.ID,

View File

@@ -58,7 +58,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
s.registerResourceRoutes(apiV1Group) s.registerResourceRoutes(apiV1Group)
s.registerMemoRoutes(apiV1Group) s.registerMemoRoutes(apiV1Group)
s.registerMemoOrganizerRoutes(apiV1Group) s.registerMemoOrganizerRoutes(apiV1Group)
s.registerMemoResourceRoutes(apiV1Group)
s.registerMemoRelationRoutes(apiV1Group) s.registerMemoRelationRoutes(apiV1Group)
// Register public routes. // Register public routes.

View File

@@ -47,12 +47,12 @@ func (s *ResourceService) ListResources(ctx context.Context, _ *apiv2pb.ListReso
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource { func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
return &apiv2pb.Resource{ return &apiv2pb.Resource{
Id: resource.ID, Id: resource.ID,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)), CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename, Filename: resource.Filename,
ExternalLink: resource.ExternalLink, ExternalLink: resource.ExternalLink,
Type: resource.Type, Type: resource.Type,
Size: resource.Size, Size: resource.Size,
RelatedMemoId: resource.RelatedMemoID, MemoId: resource.MemoID,
} }
} }

View File

@@ -20,7 +20,7 @@ message Resource {
string external_link = 4; string external_link = 4;
string type = 5; string type = 5;
int64 size = 6; int64 size = 6;
optional int32 related_memo_id = 7; optional int32 memo_id = 7;
} }
message ListResourcesRequest {} message ListResourcesRequest {}

View File

@@ -262,7 +262,7 @@
| external_link | [string](#string) | | | | external_link | [string](#string) | | |
| type | [string](#string) | | | | type | [string](#string) | | |
| size | [int64](#int64) | | | | size | [int64](#int64) | | |
| related_memo_id | [int32](#int32) | optional | | | memo_id | [int32](#int32) | optional | |

View File

@@ -27,13 +27,13 @@ type Resource struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatedTs *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"` CreatedTs *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"` Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"` ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"`
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"`
RelatedMemoId *int32 `protobuf:"varint,7,opt,name=related_memo_id,json=relatedMemoId,proto3,oneof" json:"related_memo_id,omitempty"` MemoId *int32 `protobuf:"varint,7,opt,name=memo_id,json=memoId,proto3,oneof" json:"memo_id,omitempty"`
} }
func (x *Resource) Reset() { func (x *Resource) Reset() {
@@ -110,9 +110,9 @@ func (x *Resource) GetSize() int64 {
return 0 return 0
} }
func (x *Resource) GetRelatedMemoId() int32 { func (x *Resource) GetMemoId() int32 {
if x != nil && x.RelatedMemoId != nil { if x != nil && x.MemoId != nil {
return *x.RelatedMemoId return *x.MemoId
} }
return 0 return 0
} }
@@ -211,7 +211,7 @@ var file_api_v2_resource_service_proto_rawDesc = []byte{
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xff, 0x01, 0x0a, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe8, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
@@ -223,38 +223,36 @@ var file_api_v2_resource_service_proto_rawDesc = []byte{
0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1c, 0x0a,
0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x07, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00,
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x72, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52,
0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65,
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86,
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69,
0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3,
0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65,
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e,
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65,
0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f,
0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32,
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65,
0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d,
0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41,
0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@@ -92,6 +92,7 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
Filename: attachment.FileName, Filename: attachment.FileName,
Type: attachment.GetMimeType(), Type: attachment.GetMimeType(),
Size: attachment.FileSize, Size: attachment.FileSize,
MemoID: &memoMessage.ID,
} }
err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data)) err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data))
@@ -100,20 +101,11 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
return err return err
} }
resource, err := t.store.CreateResource(ctx, &create) _, err = t.store.CreateResource(ctx, &create)
if err != nil { if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to CreateResource: %s", err), nil) _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to CreateResource: %s", err), nil)
return err return err
} }
_, err = t.store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: memoMessage.ID,
ResourceID: resource.ID,
})
if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to UpsertMemoResource: %s", err), nil)
return err
}
} }
keyboard := generateKeyboardForMemoID(memoMessage.ID) keyboard := generateKeyboardForMemoID(memoMessage.ID)

View File

@@ -81,19 +81,13 @@ CREATE TABLE resource (
external_link TEXT NOT NULL DEFAULT '', external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '', type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0, size INTEGER NOT NULL DEFAULT 0,
internal_path TEXT NOT NULL DEFAULT '' internal_path TEXT NOT NULL DEFAULT '',
memo_id INTEGER
); );
CREATE INDEX idx_resource_creator_id ON resource (creator_id); CREATE INDEX idx_resource_creator_id ON resource (creator_id);
-- memo_resource CREATE INDEX idx_resource_memo_id ON resource (memo_id);
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
-- tag -- tag
CREATE TABLE tag ( CREATE TABLE tag (

View File

@@ -0,0 +1,13 @@
ALTER TABLE resource ADD COLUMN memo_id INTEGER;
UPDATE resource
SET memo_id = (
SELECT memo_id
FROM memo_resource
WHERE resource.id = memo_resource.resource_id
LIMIT 1
);
DROP TABLE memo_resource;
CREATE INDEX idx_resource_memo_id ON resource (memo_id);

View File

@@ -175,7 +175,7 @@ func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)
memo.content AS content, memo.content AS content,
memo.visibility AS visibility, memo.visibility AS visibility,
CASE WHEN memo_organizer.pinned = 1 THEN 1 ELSE 0 END AS pinned, CASE WHEN memo_organizer.pinned = 1 THEN 1 ELSE 0 END AS pinned,
GROUP_CONCAT(memo_resource.resource_id) AS resource_id_list, GROUP_CONCAT(resource.id) AS resource_id_list,
( (
SELECT SELECT
GROUP_CONCAT(related_memo_id || ':' || type) GROUP_CONCAT(related_memo_id || ':' || type)
@@ -191,7 +191,7 @@ func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)
LEFT JOIN LEFT JOIN
memo_organizer ON memo.id = memo_organizer.memo_id memo_organizer ON memo.id = memo_organizer.memo_id
LEFT JOIN LEFT JOIN
memo_resource ON memo.id = memo_resource.memo_id resource ON memo.id = resource.memo_id
WHERE ` + strings.Join(where, " AND ") + ` WHERE ` + strings.Join(where, " AND ") + `
GROUP BY memo.id GROUP BY memo.id
ORDER BY ` + strings.Join(orders, ", ") + ` ORDER BY ` + strings.Join(orders, ", ") + `

View File

@@ -1,168 +0,0 @@
package store
import (
"context"
"database/sql"
"strings"
)
type MemoResource struct {
MemoID int32
ResourceID int32
CreatedTs int64
UpdatedTs int64
}
type UpsertMemoResource struct {
MemoID int32
ResourceID int32
CreatedTs int64
UpdatedTs *int64
}
type FindMemoResource struct {
MemoID *int32
ResourceID *int32
}
type DeleteMemoResource struct {
MemoID *int32
ResourceID *int32
}
func (s *Store) UpsertMemoResource(ctx context.Context, upsert *UpsertMemoResource) (*MemoResource, error) {
set := []string{"memo_id", "resource_id"}
args := []any{upsert.MemoID, upsert.ResourceID}
placeholder := []string{"?", "?"}
if v := upsert.UpdatedTs; v != nil {
set, args, placeholder = append(set, "updated_ts"), append(args, v), append(placeholder, "?")
}
query := `
INSERT INTO memo_resource (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
ON CONFLICT(memo_id, resource_id) DO UPDATE
SET
updated_ts = EXCLUDED.updated_ts
RETURNING memo_id, resource_id, created_ts, updated_ts
`
memoResource := &MemoResource{}
if err := s.db.QueryRowContext(ctx, query, args...).Scan(
&memoResource.MemoID,
&memoResource.ResourceID,
&memoResource.CreatedTs,
&memoResource.UpdatedTs,
); err != nil {
return nil, err
}
return memoResource, nil
}
func (s *Store) ListMemoResources(ctx context.Context, find *FindMemoResource) ([]*MemoResource, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := find.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
query := `
SELECT
memo_id,
resource_id,
created_ts,
updated_ts
FROM memo_resource
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY updated_ts DESC
`
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]*MemoResource, 0)
for rows.Next() {
var memoResource MemoResource
if err := rows.Scan(
&memoResource.MemoID,
&memoResource.ResourceID,
&memoResource.CreatedTs,
&memoResource.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, &memoResource)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (s *Store) GetMemoResource(ctx context.Context, find *FindMemoResource) (*MemoResource, error) {
list, err := s.ListMemoResources(ctx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
memoResource := list[0]
return memoResource, nil
}
func (s *Store) DeleteMemoResource(ctx context.Context, delete *DeleteMemoResource) error {
where, args := []string{}, []any{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
result, err := s.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
if _, err = result.RowsAffected(); err != nil {
return err
}
return nil
}
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_resource
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR resource_id NOT IN (
SELECT
id
FROM
resource
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return err
}
return nil
}

View File

@@ -20,9 +20,7 @@ type Resource struct {
ExternalLink string ExternalLink string
Type string Type string
Size int64 Size int64
MemoID *int32
// Related fields
RelatedMemoID *int32
} }
type FindResource struct { type FindResource struct {
@@ -41,11 +39,14 @@ type UpdateResource struct {
UpdatedTs *int64 UpdatedTs *int64
Filename *string Filename *string
InternalPath *string InternalPath *string
MemoID *int32
UnbindMemo bool
Blob []byte Blob []byte
} }
type DeleteResource struct { type DeleteResource struct {
ID int32 ID int32
MemoID *int32
} }
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) { func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
@@ -45,35 +44,33 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "resource.id = ?"), append(args, *v) where, args = append(where, "id = ?"), append(args, *v)
} }
if v := find.CreatorID; v != nil { if v := find.CreatorID; v != nil {
where, args = append(where, "resource.creator_id = ?"), append(args, *v) where, args = append(where, "creator_id = ?"), append(args, *v)
} }
if v := find.Filename; v != nil { if v := find.Filename; v != nil {
where, args = append(where, "resource.filename = ?"), append(args, *v) where, args = append(where, "filename = ?"), append(args, *v)
} }
if v := find.MemoID; v != nil { if v := find.MemoID; v != nil {
where, args = append(where, "resource.id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v) where, args = append(where, "memo_id = ?"), append(args, *v)
} }
if find.HasRelatedMemo { if find.HasRelatedMemo {
where = append(where, "memo_resource.memo_id IS NOT NULL") where = append(where, "memo_id IS NOT NULL")
} }
fields := []string{"resource.id", "resource.filename", "resource.external_link", "resource.type", "resource.size", "resource.creator_id", "resource.created_ts", "resource.updated_ts", "internal_path"} fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts", "internal_path", "memo_id"}
if find.GetBlob { if find.GetBlob {
fields = append(fields, "resource.blob") fields = append(fields, "blob")
} }
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT SELECT
GROUP_CONCAT(memo_resource.memo_id) as related_memo_ids,
%s %s
FROM resource FROM resource
LEFT JOIN memo_resource ON resource.id = memo_resource.resource_id
WHERE %s WHERE %s
GROUP BY resource.id GROUP BY id
ORDER BY resource.created_ts DESC ORDER BY created_ts DESC
`, strings.Join(fields, ", "), strings.Join(where, " AND ")) `, strings.Join(fields, ", "), strings.Join(where, " AND "))
if find.Limit != nil { if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
@@ -91,9 +88,8 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
list := make([]*store.Resource, 0) list := make([]*store.Resource, 0)
for rows.Next() { for rows.Next() {
resource := store.Resource{} resource := store.Resource{}
var relatedMemoIDs sql.NullString var memoID sql.NullInt32
dests := []any{ dests := []any{
&relatedMemoIDs,
&resource.ID, &resource.ID,
&resource.Filename, &resource.Filename,
&resource.ExternalLink, &resource.ExternalLink,
@@ -103,6 +99,7 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
&resource.CreatedTs, &resource.CreatedTs,
&resource.UpdatedTs, &resource.UpdatedTs,
&resource.InternalPath, &resource.InternalPath,
&memoID,
} }
if find.GetBlob { if find.GetBlob {
dests = append(dests, &resource.Blob) dests = append(dests, &resource.Blob)
@@ -110,17 +107,8 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
if err := rows.Scan(dests...); err != nil { if err := rows.Scan(dests...); err != nil {
return nil, err return nil, err
} }
if relatedMemoIDs.Valid { if memoID.Valid {
relatedMemoIDList := strings.Split(relatedMemoIDs.String, ",") resource.MemoID = &memoID.Int32
if len(relatedMemoIDList) > 0 {
// Only take the first related memo ID.
relatedMemoIDInt, err := strconv.ParseInt(relatedMemoIDList[0], 10, 32)
if err != nil {
return nil, err
}
relatedMemoID := int32(relatedMemoIDInt)
resource.RelatedMemoID = &relatedMemoID
}
} }
list = append(list, &resource) list = append(list, &resource)
} }
@@ -144,6 +132,12 @@ func (d *Driver) UpdateResource(ctx context.Context, update *store.UpdateResourc
if v := update.InternalPath; v != nil { if v := update.InternalPath; v != nil {
set, args = append(set, "internal_path = ?"), append(args, *v) set, args = append(set, "internal_path = ?"), append(args, *v)
} }
if v := update.MemoID; v != nil {
set, args = append(set, "memo_id = ?"), append(args, *v)
}
if update.UnbindMemo {
set = append(set, "memo_id = NULL")
}
if v := update.Blob; v != nil { if v := update.Blob; v != nil {
set, args = append(set, "blob = ?"), append(args, v) set, args = append(set, "blob = ?"), append(args, v)
} }

View File

@@ -109,9 +109,6 @@ func (*Store) vacuumImpl(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemoOrganizer(ctx, tx); err != nil { if err := vacuumMemoOrganizer(ctx, tx); err != nil {
return err return err
} }
if err := vacuumMemoResource(ctx, tx); err != nil {
return err
}
if err := vacuumMemoRelations(ctx, tx); err != nil { if err := vacuumMemoRelations(ctx, tx); err != nil {
return err return err
} }

View File

@@ -31,17 +31,6 @@ func TestResourceStore(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, correctFilename, resource.Filename) require.Equal(t, correctFilename, resource.Filename)
require.Equal(t, int32(1), resource.ID) require.Equal(t, int32(1), resource.ID)
_, err = ts.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: 1,
ResourceID: resource.ID,
})
require.NoError(t, err)
resource, err = ts.GetResource(ctx, &store.FindResource{
ID: &resource.ID,
})
require.NoError(t, err)
require.Equal(t, *resource.RelatedMemoID, int32(1))
notFoundResource, err := ts.GetResource(ctx, &store.FindResource{ notFoundResource, err := ts.GetResource(ctx, &store.FindResource{
Filename: &incorrectFilename, Filename: &incorrectFilename,

View File

@@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useLocalStorage from "react-use/lib/useLocalStorage"; import useLocalStorage from "react-use/lib/useLocalStorage";
import { upsertMemoResource } from "@/helpers/api";
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts"; import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
import { clearContentQueryParam } from "@/helpers/utils"; import { clearContentQueryParam } from "@/helpers/utils";
import { getMatchedNodes } from "@/labs/marked"; import { getMatchedNodes } from "@/labs/marked";
@@ -223,7 +222,10 @@ const MemoEditor = (props: Props) => {
if (resource) { if (resource) {
uploadedResourceList.push(resource); uploadedResourceList.push(resource);
if (memoId) { if (memoId) {
await upsertMemoResource(memoId, resource.id); await resourceStore.patchResource({
id: resource.id,
memoId,
});
} }
} }
} }

View File

@@ -155,20 +155,6 @@ export function deleteResourceById(id: ResourceId) {
return axios.delete(`/api/v1/resource/${id}`); return axios.delete(`/api/v1/resource/${id}`);
} }
export function getMemoResourceList(memoId: MemoId) {
return axios.get<Resource[]>(`/api/v1/memo/${memoId}/resource`);
}
export function upsertMemoResource(memoId: MemoId, resourceId: ResourceId) {
return axios.post(`/api/v1/memo/${memoId}/resource`, {
resourceId,
});
}
export function deleteMemoResource(memoId: MemoId, resourceId: ResourceId) {
return axios.delete(`/api/v1/memo/${memoId}/resource/${resourceId}`);
}
export function getTagList() { export function getTagList() {
return axios.get<string[]>(`/api/v1/tag`); return axios.get<string[]>(`/api/v1/tag`);
} }

View File

@@ -40,31 +40,14 @@ export const useResourceStore = () => {
store.dispatch(setResources([resource, ...resourceList])); store.dispatch(setResources([resource, ...resourceList]));
return resource; return resource;
}, },
async createResourcesWithBlob(files: FileList): Promise<Array<Resource>> {
let newResourceList: Array<Resource> = [];
for (const file of files) {
const { name: filename, size } = file;
if (size > maxUploadSizeMiB * 1024 * 1024) {
return Promise.reject(t("message.file-exceeds-upload-limit-of", { file: filename, size: maxUploadSizeMiB }));
}
const formData = new FormData();
formData.append("file", file, filename);
const { data: resource } = await api.createResourceWithBlob(formData);
newResourceList = [resource, ...newResourceList];
}
const resourceList = state.resources;
store.dispatch(setResources([...newResourceList, ...resourceList]));
return newResourceList;
},
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> { async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data: resource } = await api.patchResource(resourcePatch); const { data: resource } = await api.patchResource(resourcePatch);
store.dispatch(patchResource(resource)); store.dispatch(patchResource(resource));
return resource; return resource;
}, },
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
}; };
}; };

View File

@@ -9,6 +9,7 @@ interface ResourceCreate {
interface ResourcePatch { interface ResourcePatch {
id: ResourceId; id: ResourceId;
filename?: string; filename?: string;
memoId?: number;
} }
interface ResourceFind { interface ResourceFind {