feat: implement memo relation server (#1618)

This commit is contained in:
boojack 2023-05-01 16:09:41 +08:00 committed by GitHub
parent 6e6aae6649
commit b6564bcd77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 276 additions and 8 deletions

View file

@ -42,8 +42,9 @@ type Memo struct {
Pinned bool `json:"pinned"`
// Related fields
CreatorName string `json:"creatorName"`
ResourceList []*Resource `json:"resourceList"`
CreatorName string `json:"creatorName"`
ResourceList []*Resource `json:"resourceList"`
RelationList []*MemoRelation `json:"relationList"`
}
type MemoCreate struct {
@ -56,7 +57,8 @@ type MemoCreate struct {
Content string `json:"content"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
ResourceIDList []int `json:"resourceIdList"`
MemoRelationList []*MemoRelationUpsert `json:"memoRelationList"`
}
type MemoPatch struct {
@ -72,7 +74,8 @@ type MemoPatch struct {
Visibility *Visibility `json:"visibility"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
ResourceIDList []int `json:"resourceIdList"`
MemoRelationList []*MemoRelationUpsert `json:"memoRelationList"`
}
type MemoFind struct {

19
api/memo_relation.go Normal file
View file

@ -0,0 +1,19 @@
package api
type MemoRelationType string
const (
MemoRelationReference MemoRelationType = "REFERENCE"
MemoRelationAdditional MemoRelationType = "ADDITIONAL"
)
type MemoRelation struct {
MemoID int
RelatedMemoID int
Type MemoRelationType
}
type MemoRelationUpsert struct {
RelatedMemoID int
Type MemoRelationType
}

View file

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/store"
"github.com/labstack/echo/v4"
)
@ -101,6 +102,18 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
}
if s.Profile.IsDev() {
for _, memoRelationUpsert := range memoCreate.MemoRelationList {
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
MemoID: memo.ID,
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
Type: store.MemoRelationType(memoRelationUpsert.Type),
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
}
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
@ -157,6 +170,18 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
}
if s.Profile.IsDev() {
for _, memoRelationUpsert := range memoPatch.MemoRelationList {
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
MemoID: memo.ID,
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
Type: store.MemoRelationType(memoRelationUpsert.Type),
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
}
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)

76
server/memo_relation.go Normal file
View file

@ -0,0 +1,76 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/store"
"github.com/labstack/echo/v4"
)
func (s *Server) registerMemoRelationRoutes(g *echo.Group) {
g.POST("/memo/:memoId/relation", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoRelationUpsert := &api.MemoRelationUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(memoRelationUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo relation request").SetInternal(err)
}
memoRelation, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelationMessage{
MemoID: memoID,
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
Type: store.MemoRelationType(memoRelationUpsert.Type),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(memoRelation))
})
g.GET("/memo/:memoId/relation", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelationMessage{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(memoRelationList))
})
g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(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)
}
relatedMemoID, err := strconv.Atoi(c.Param("relatedMemoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
relationType := store.MemoRelationType(c.Param("relationType"))
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelationMessage{
MemoID: &memoID,
RelatedMemoID: &relatedMemoID,
Type: &relationType,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}

View file

@ -109,6 +109,7 @@ func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) {
s.registerStorageRoutes(apiGroup)
s.registerIdentityProviderRoutes(apiGroup)
s.registerOpenAIRoutes(apiGroup)
s.registerMemoRelationRoutes(apiGroup)
return s, nil
}

View file

@ -53,6 +53,11 @@ func (s *Store) ComposeMemo(ctx context.Context, memo *api.Memo) (*api.Memo, err
if err := s.ComposeMemoResourceList(ctx, memo); err != nil {
return nil, err
}
if s.profile.IsDev() {
if err := s.ComposeMemoRelationList(ctx, memo); err != nil {
return nil, err
}
}
return memo, nil
}

View file

@ -6,9 +6,29 @@ import (
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
func (s *Store) ComposeMemoRelationList(ctx context.Context, memo *api.Memo) error {
memoRelationList, err := s.ListMemoRelations(ctx, &FindMemoRelationMessage{
MemoID: &memo.ID,
})
if err != nil {
return err
}
for _, memoRelation := range memoRelationList {
memo.RelationList = append(memo.RelationList, &api.MemoRelation{
MemoID: memoRelation.MemoID,
RelatedMemoID: memoRelation.RelatedMemoID,
Type: api.MemoRelationType(memoRelation.Type),
})
}
return nil
}
type MemoRelationType string
const (

View file

@ -0,0 +1,97 @@
package testserver
import (
"bytes"
"context"
"encoding/json"
"fmt"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/api"
)
func TestMemoRelationServer(t *testing.T) {
ctx := context.Background()
s, err := NewTestingServer(ctx, t)
require.NoError(t, err)
defer s.Shutdown(ctx)
signup := &api.SignUp{
Username: "testuser",
Password: "testpassword",
}
user, err := s.postAuthSignup(signup)
require.NoError(t, err)
require.Equal(t, signup.Username, user.Username)
memoList, err := s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 0)
memo, err := s.postMemoCreate(&api.MemoCreate{
Content: "test memo",
})
require.NoError(t, err)
require.Equal(t, "test memo", memo.Content)
memo2, err := s.postMemoCreate(&api.MemoCreate{
Content: "test memo2",
MemoRelationList: []*api.MemoRelationUpsert{
{
RelatedMemoID: memo.ID,
Type: api.MemoRelationReference,
},
},
})
require.NoError(t, err)
require.Equal(t, "test memo2", memo2.Content)
memoList, err = s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 2)
require.Len(t, memo2.RelationList, 1)
err = s.deleteMemoRelation(memo2.ID, memo.ID, api.MemoRelationReference)
require.NoError(t, err)
memo2, err = s.getMemo(memo2.ID)
require.NoError(t, err)
require.Len(t, memo2.RelationList, 0)
memoRelation, err := s.postMemoRelationUpsert(memo2.ID, &api.MemoRelationUpsert{
RelatedMemoID: memo.ID,
Type: api.MemoRelationReference,
})
require.NoError(t, err)
require.Equal(t, memo.ID, memoRelation.RelatedMemoID)
memo2, err = s.getMemo(memo2.ID)
require.NoError(t, err)
require.Len(t, memo2.RelationList, 1)
}
func (s *TestingServer) postMemoRelationUpsert(memoID int, memoRelationUpsert *api.MemoRelationUpsert) (*api.MemoRelation, error) {
rawData, err := json.Marshal(&memoRelationUpsert)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal memo relation upsert")
}
reader := bytes.NewReader(rawData)
body, err := s.post(fmt.Sprintf("/api/memo/%d/relation", memoID), reader, nil)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(body)
if err != nil {
return nil, errors.Wrap(err, "fail to read response body")
}
type MemoCreateResponse struct {
Data *api.MemoRelation `json:"data"`
}
res := new(MemoCreateResponse)
if err = json.Unmarshal(buf.Bytes(), res); err != nil {
return nil, errors.Wrap(err, "fail to unmarshal post memo relation upsert response")
}
return res.Data, nil
}
func (s *TestingServer) deleteMemoRelation(memoID int, relatedMemoID int, relationType api.MemoRelationType) error {
_, err := s.delete(fmt.Sprintf("/api/memo/%d/relation/%d/type/%s", memoID, relatedMemoID, relationType), nil)
return err
}

View file

@ -37,13 +37,13 @@ func TestMemoServer(t *testing.T) {
require.NoError(t, err)
require.Len(t, memoList, 1)
updatedContent := "updated memo"
memo, err = s.patchMemoPatch(&api.MemoPatch{
memo, err = s.patchMemo(&api.MemoPatch{
ID: memo.ID,
Content: &updatedContent,
})
require.NoError(t, err)
require.Equal(t, updatedContent, memo.Content)
err = s.postMemoDelete(&api.MemoDelete{
err = s.deleteMemo(&api.MemoDelete{
ID: memo.ID,
})
require.NoError(t, err)
@ -52,6 +52,28 @@ func TestMemoServer(t *testing.T) {
require.Len(t, memoList, 0)
}
func (s *TestingServer) getMemo(memoID int) (*api.Memo, error) {
body, err := s.get(fmt.Sprintf("/api/memo/%d", memoID), nil)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(body)
if err != nil {
return nil, errors.Wrap(err, "fail to read response body")
}
type MemoCreateResponse struct {
Data *api.Memo `json:"data"`
}
res := new(MemoCreateResponse)
if err = json.Unmarshal(buf.Bytes(), res); err != nil {
return nil, errors.Wrap(err, "fail to unmarshal get memo response")
}
return res.Data, nil
}
func (s *TestingServer) getMemoList() ([]*api.Memo, error) {
body, err := s.get("/api/memo", nil)
if err != nil {
@ -101,7 +123,7 @@ func (s *TestingServer) postMemoCreate(memoCreate *api.MemoCreate) (*api.Memo, e
return res.Data, nil
}
func (s *TestingServer) patchMemoPatch(memoPatch *api.MemoPatch) (*api.Memo, error) {
func (s *TestingServer) patchMemo(memoPatch *api.MemoPatch) (*api.Memo, error) {
rawData, err := json.Marshal(&memoPatch)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal memo patch")
@ -128,7 +150,7 @@ func (s *TestingServer) patchMemoPatch(memoPatch *api.MemoPatch) (*api.Memo, err
return res.Data, nil
}
func (s *TestingServer) postMemoDelete(memoDelete *api.MemoDelete) error {
func (s *TestingServer) deleteMemo(memoDelete *api.MemoDelete) error {
_, err := s.delete(fmt.Sprintf("/api/memo/%d", memoDelete.ID), nil)
return err
}