memos/api/v1/memo_resource.go
Lincoln Nogueira 4491c75135
feat: add SwaggerUI and v1 API docs (#2115)
* - Refactor several API routes from anonymous functions to regular definitions. Required to add parseable documentation comments.

- Add API documentation comments using Swag Declarative Comments Format

- Add echo-swagger to serve Swagger-UI at /api/index.html

- Fix error response from extraneous parameter resourceId to relatedMemoId in DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType")

- Add an auto-generated ./docs/api/v1.md for quick reference on repo (generated by swagger-markdown)

- Add auxiliary scripts to generate docs.go and swagger.yaml

* fix: golangci-lint errors

* fix: go fmt flag in swag scripts
2023-08-09 21:53:06 +08:00

182 lines
6.4 KiB
Go

package v1
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api/auth"
"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"
// @Security ApiKeyAuth
// @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(auth.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"
// @Security ApiKeyAuth
// @Router /api/v1/memo/{memoId}/resource/{resourceId} [DELETE]
func (s *APIV1Service) unbindMemoResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.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)
}