mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-08 05:47:07 +09:00
GO-4459: Refactor into auth, object, search, utils
This commit is contained in:
parent
c0f69df4b9
commit
5f7355f496
19 changed files with 1433 additions and 1150 deletions
69
cmd/api/auth/handler.go
Normal file
69
cmd/api/auth/handler.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
)
|
||||
|
||||
// AuthDisplayCodeHandler generates a new challenge and returns the challenge ID
|
||||
//
|
||||
// @Summary Open a modal window with a code in Anytype Desktop app
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} AuthDisplayCodeResponse "Challenge ID"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /auth/displayCode [post]
|
||||
func AuthDisplayCodeHandler(s *AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
challengeId, err := s.GenerateNewChallenge(c.Request.Context(), "api-test")
|
||||
code := util.MapErrorCode(err, util.ErrToCode(ErrFailedGenerateChallenge, http.StatusInternalServerError))
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthDisplayCodeResponse{ChallengeId: challengeId})
|
||||
}
|
||||
}
|
||||
|
||||
// AuthTokenHandler retrieves an authentication token using a code and challenge ID
|
||||
//
|
||||
// @Summary Retrieve an authentication token using a code
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "The code retrieved from Anytype Desktop app"
|
||||
// @Param challenge_id query string true "The challenge ID"
|
||||
// @Success 200 {object} AuthTokenResponse "Authentication token"
|
||||
// @Failure 400 {object} util.ValidationError "Invalid input"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /auth/token [get]
|
||||
func AuthTokenHandler(s *AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
challengeID := c.Query("challenge_id")
|
||||
code := c.Query("code")
|
||||
|
||||
sessionToken, appKey, err := s.SolveChallengeForToken(c.Request.Context(), challengeID, code)
|
||||
errCode := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrInvalidInput, http.StatusBadRequest),
|
||||
util.ErrToCode(ErrorFailedAuthenticate, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if errCode != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(errCode, err.Error())
|
||||
c.JSON(errCode, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthTokenResponse{
|
||||
SessionToken: sessionToken,
|
||||
AppKey: appKey,
|
||||
})
|
||||
}
|
||||
}
|
10
cmd/api/auth/model.go
Normal file
10
cmd/api/auth/model.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package auth
|
||||
|
||||
type AuthDisplayCodeResponse struct {
|
||||
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"`
|
||||
}
|
||||
|
||||
type AuthTokenResponse struct {
|
||||
SessionToken string `json:"session_token" example:""`
|
||||
AppKey string `json:"app_key" example:""`
|
||||
}
|
60
cmd/api/auth/service.go
Normal file
60
cmd/api/auth/service.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedGenerateChallenge = errors.New("failed to generate a new challenge")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrorFailedAuthenticate = errors.New("failed to authenticate user")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GenerateNewChallenge(ctx context.Context, appName string) (string, error)
|
||||
SolveChallengeForToken(ctx context.Context, challengeID, code string) (sessionToken, appKey string, err error)
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
mw service.ClientCommandsServer
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *AuthService {
|
||||
return &AuthService{mw: mw}
|
||||
}
|
||||
|
||||
// GenerateNewChallenge calls mw.AccountLocalLinkNewChallenge(...)
|
||||
// and returns the challenge ID, or an error if it fails.
|
||||
func (s *AuthService) GenerateNewChallenge(ctx context.Context, appName string) (string, error) {
|
||||
resp := s.mw.AccountLocalLinkNewChallenge(ctx, &pb.RpcAccountLocalLinkNewChallengeRequest{AppName: "api-test"})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkNewChallengeResponseError_NULL {
|
||||
return "", ErrFailedGenerateChallenge
|
||||
}
|
||||
|
||||
return resp.ChallengeId, nil
|
||||
}
|
||||
|
||||
// SolveChallengeForToken calls mw.AccountLocalLinkSolveChallenge(...)
|
||||
// and returns the session token + app key, or an error if it fails.
|
||||
func (s *AuthService) SolveChallengeForToken(ctx context.Context, challengeID, code string) (sessionToken, appKey string, err error) {
|
||||
if challengeID == "" || code == "" {
|
||||
return "", "", ErrInvalidInput
|
||||
}
|
||||
|
||||
// Call AccountLocalLinkSolveChallenge to retrieve session token and app key
|
||||
resp := s.mw.AccountLocalLinkSolveChallenge(ctx, &pb.RpcAccountLocalLinkSolveChallengeRequest{
|
||||
ChallengeId: challengeID,
|
||||
Answer: code,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL {
|
||||
return "", "", ErrorFailedAuthenticate
|
||||
}
|
||||
|
||||
return resp.SessionToken, resp.AppKey, nil
|
||||
}
|
|
@ -40,13 +40,13 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "Challenge ID",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.AuthDisplayCodeResponse"
|
||||
"$ref": "#/definitions/auth.AuthDisplayCodeResponse"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,19 +84,19 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "Authentication token",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.AuthTokenResponse"
|
||||
"$ref": "#/definitions/auth.AuthTokenResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ValidationError"
|
||||
"$ref": "#/definitions/util.ValidationError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ const docTemplate = `{
|
|||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Specify object type for search",
|
||||
"description": "Specify object.Object type for search",
|
||||
"name": "object_type",
|
||||
"in": "query"
|
||||
},
|
||||
|
@ -149,7 +149,7 @@ const docTemplate = `{
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,13 +157,19 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,19 +212,19 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,13 +261,13 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,19 +317,19 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -369,26 +375,26 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/api.ObjectType"
|
||||
"$ref": "#/definitions/object.ObjectType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +449,7 @@ const docTemplate = `{
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ObjectTemplate"
|
||||
"$ref": "#/definitions/object.ObjectTemplate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -451,19 +457,19 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +517,7 @@ const docTemplate = `{
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -519,19 +525,19 @@ const docTemplate = `{
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -572,19 +578,19 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "The created object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -622,25 +628,25 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "The requested object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -677,7 +683,7 @@ const docTemplate = `{
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -685,25 +691,25 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "The updated object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -711,7 +717,7 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.AuthDisplayCodeResponse": {
|
||||
"auth.AuthDisplayCodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"challenge_id": {
|
||||
|
@ -720,7 +726,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.AuthTokenResponse": {
|
||||
"auth.AuthTokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_key": {
|
||||
|
@ -733,7 +739,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.Block": {
|
||||
"object.Block": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"align": {
|
||||
|
@ -749,20 +755,20 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"file": {
|
||||
"$ref": "#/definitions/api.File"
|
||||
"$ref": "#/definitions/object.File"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"$ref": "#/definitions/api.Text"
|
||||
"$ref": "#/definitions/object.Text"
|
||||
},
|
||||
"vertical_align": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Detail": {
|
||||
"object.Detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {
|
||||
|
@ -774,7 +780,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.File": {
|
||||
"object.File": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"added_at": {
|
||||
|
@ -806,32 +812,19 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.NotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Object": {
|
||||
"object.Object": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"blocks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Block"
|
||||
"$ref": "#/definitions/object.Block"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Detail"
|
||||
"$ref": "#/definitions/object.Detail"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
|
@ -863,7 +856,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ObjectTemplate": {
|
||||
"object.ObjectTemplate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
|
@ -884,7 +877,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ObjectType": {
|
||||
"object.ObjectType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
|
@ -909,20 +902,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ServerError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Text": {
|
||||
"object.Text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"checked": {
|
||||
|
@ -942,32 +922,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.UnauthorizedError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pagination.PaginatedResponse-space_Member": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1071,7 +1025,7 @@ const docTemplate = `{
|
|||
},
|
||||
"analytics_id": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "624aecdd-4797-4611-9d61-a2ae5f53cf1c"
|
||||
},
|
||||
"archive_object_id": {
|
||||
"type": "string",
|
||||
|
@ -1083,7 +1037,7 @@ const docTemplate = `{
|
|||
},
|
||||
"gateway_url": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "http://127.0.0.1:31006"
|
||||
},
|
||||
"home_object_id": {
|
||||
"type": "string",
|
||||
|
@ -1099,7 +1053,7 @@ const docTemplate = `{
|
|||
},
|
||||
"local_storage_path": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"
|
||||
},
|
||||
"marketplace_workspace_id": {
|
||||
"type": "string",
|
||||
|
@ -1142,6 +1096,58 @@ const docTemplate = `{
|
|||
"example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.NotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.ServerError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.UnauthorizedError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
|
|
@ -34,13 +34,13 @@
|
|||
"200": {
|
||||
"description": "Challenge ID",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.AuthDisplayCodeResponse"
|
||||
"$ref": "#/definitions/auth.AuthDisplayCodeResponse"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,19 +78,19 @@
|
|||
"200": {
|
||||
"description": "Authentication token",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.AuthTokenResponse"
|
||||
"$ref": "#/definitions/auth.AuthTokenResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ValidationError"
|
||||
"$ref": "#/definitions/util.ValidationError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@
|
|||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Specify object type for search",
|
||||
"description": "Specify object.Object type for search",
|
||||
"name": "object_type",
|
||||
"in": "query"
|
||||
},
|
||||
|
@ -143,7 +143,7 @@
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,13 +151,19 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,19 +206,19 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,13 +255,13 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,19 +311,19 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -363,26 +369,26 @@
|
|||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/api.ObjectType"
|
||||
"$ref": "#/definitions/object.ObjectType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -437,7 +443,7 @@
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ObjectTemplate"
|
||||
"$ref": "#/definitions/object.ObjectTemplate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -445,19 +451,19 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -505,7 +511,7 @@
|
|||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -513,19 +519,19 @@
|
|||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -566,19 +572,19 @@
|
|||
"200": {
|
||||
"description": "The created object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -616,25 +622,25 @@
|
|||
"200": {
|
||||
"description": "The requested object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -671,7 +677,7 @@
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -679,25 +685,25 @@
|
|||
"200": {
|
||||
"description": "The updated object",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.Object"
|
||||
"$ref": "#/definitions/object.Object"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.UnauthorizedError"
|
||||
"$ref": "#/definitions/util.UnauthorizedError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.NotFoundError"
|
||||
"$ref": "#/definitions/util.NotFoundError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ServerError"
|
||||
"$ref": "#/definitions/util.ServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -705,7 +711,7 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.AuthDisplayCodeResponse": {
|
||||
"auth.AuthDisplayCodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"challenge_id": {
|
||||
|
@ -714,7 +720,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.AuthTokenResponse": {
|
||||
"auth.AuthTokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_key": {
|
||||
|
@ -727,7 +733,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.Block": {
|
||||
"object.Block": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"align": {
|
||||
|
@ -743,20 +749,20 @@
|
|||
}
|
||||
},
|
||||
"file": {
|
||||
"$ref": "#/definitions/api.File"
|
||||
"$ref": "#/definitions/object.File"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"$ref": "#/definitions/api.Text"
|
||||
"$ref": "#/definitions/object.Text"
|
||||
},
|
||||
"vertical_align": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Detail": {
|
||||
"object.Detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {
|
||||
|
@ -768,7 +774,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.File": {
|
||||
"object.File": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"added_at": {
|
||||
|
@ -800,32 +806,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.NotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Object": {
|
||||
"object.Object": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"blocks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Block"
|
||||
"$ref": "#/definitions/object.Block"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.Detail"
|
||||
"$ref": "#/definitions/object.Detail"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
|
@ -857,7 +850,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ObjectTemplate": {
|
||||
"object.ObjectTemplate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
|
@ -878,7 +871,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ObjectType": {
|
||||
"object.ObjectType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
|
@ -903,20 +896,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.ServerError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.Text": {
|
||||
"object.Text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"checked": {
|
||||
|
@ -936,32 +916,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"api.UnauthorizedError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pagination.PaginatedResponse-space_Member": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1065,7 +1019,7 @@
|
|||
},
|
||||
"analytics_id": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "624aecdd-4797-4611-9d61-a2ae5f53cf1c"
|
||||
},
|
||||
"archive_object_id": {
|
||||
"type": "string",
|
||||
|
@ -1077,7 +1031,7 @@
|
|||
},
|
||||
"gateway_url": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "http://127.0.0.1:31006"
|
||||
},
|
||||
"home_object_id": {
|
||||
"type": "string",
|
||||
|
@ -1093,7 +1047,7 @@
|
|||
},
|
||||
"local_storage_path": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
"example": "/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"
|
||||
},
|
||||
"marketplace_workspace_id": {
|
||||
"type": "string",
|
||||
|
@ -1136,6 +1090,58 @@
|
|||
"example": "bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.NotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.ServerError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.UnauthorizedError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
basePath: /v1
|
||||
definitions:
|
||||
api.AuthDisplayCodeResponse:
|
||||
auth.AuthDisplayCodeResponse:
|
||||
properties:
|
||||
challenge_id:
|
||||
example: 67647f5ecda913e9a2e11b26
|
||||
type: string
|
||||
type: object
|
||||
api.AuthTokenResponse:
|
||||
auth.AuthTokenResponse:
|
||||
properties:
|
||||
app_key:
|
||||
example: ""
|
||||
|
@ -15,7 +15,7 @@ definitions:
|
|||
example: ""
|
||||
type: string
|
||||
type: object
|
||||
api.Block:
|
||||
object.Block:
|
||||
properties:
|
||||
align:
|
||||
type: string
|
||||
|
@ -26,15 +26,15 @@ definitions:
|
|||
type: string
|
||||
type: array
|
||||
file:
|
||||
$ref: '#/definitions/api.File'
|
||||
$ref: '#/definitions/object.File'
|
||||
id:
|
||||
type: string
|
||||
text:
|
||||
$ref: '#/definitions/api.Text'
|
||||
$ref: '#/definitions/object.Text'
|
||||
vertical_align:
|
||||
type: string
|
||||
type: object
|
||||
api.Detail:
|
||||
object.Detail:
|
||||
properties:
|
||||
details:
|
||||
additionalProperties: true
|
||||
|
@ -42,7 +42,7 @@ definitions:
|
|||
id:
|
||||
type: string
|
||||
type: object
|
||||
api.File:
|
||||
object.File:
|
||||
properties:
|
||||
added_at:
|
||||
type: integer
|
||||
|
@ -63,23 +63,15 @@ definitions:
|
|||
type:
|
||||
type: string
|
||||
type: object
|
||||
api.NotFoundError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
api.Object:
|
||||
object.Object:
|
||||
properties:
|
||||
blocks:
|
||||
items:
|
||||
$ref: '#/definitions/api.Block'
|
||||
$ref: '#/definitions/object.Block'
|
||||
type: array
|
||||
details:
|
||||
items:
|
||||
$ref: '#/definitions/api.Detail'
|
||||
$ref: '#/definitions/object.Detail'
|
||||
type: array
|
||||
icon:
|
||||
example: "\U0001F4C4"
|
||||
|
@ -102,7 +94,7 @@ definitions:
|
|||
example: object
|
||||
type: string
|
||||
type: object
|
||||
api.ObjectTemplate:
|
||||
object.ObjectTemplate:
|
||||
properties:
|
||||
icon:
|
||||
example: "\U0001F4C4"
|
||||
|
@ -117,7 +109,7 @@ definitions:
|
|||
example: object_template
|
||||
type: string
|
||||
type: object
|
||||
api.ObjectType:
|
||||
object.ObjectType:
|
||||
properties:
|
||||
icon:
|
||||
example: "\U0001F4C4"
|
||||
|
@ -135,15 +127,7 @@ definitions:
|
|||
example: ot-page
|
||||
type: string
|
||||
type: object
|
||||
api.ServerError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
api.Text:
|
||||
object.Text:
|
||||
properties:
|
||||
checked:
|
||||
type: boolean
|
||||
|
@ -156,22 +140,6 @@ definitions:
|
|||
text:
|
||||
type: string
|
||||
type: object
|
||||
api.UnauthorizedError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
api.ValidationError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
pagination.PaginatedResponse-space_Member:
|
||||
properties:
|
||||
data:
|
||||
|
@ -244,7 +212,7 @@ definitions:
|
|||
example: bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1
|
||||
type: string
|
||||
analytics_id:
|
||||
example: ""
|
||||
example: 624aecdd-4797-4611-9d61-a2ae5f53cf1c
|
||||
type: string
|
||||
archive_object_id:
|
||||
example: bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri
|
||||
|
@ -253,7 +221,7 @@ definitions:
|
|||
example: 12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF
|
||||
type: string
|
||||
gateway_url:
|
||||
example: ""
|
||||
example: http://127.0.0.1:31006
|
||||
type: string
|
||||
home_object_id:
|
||||
example: bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya
|
||||
|
@ -265,7 +233,7 @@ definitions:
|
|||
example: bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1
|
||||
type: string
|
||||
local_storage_path:
|
||||
example: ""
|
||||
example: /Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha
|
||||
type: string
|
||||
marketplace_workspace_id:
|
||||
example: _anytype_marketplace
|
||||
|
@ -298,6 +266,38 @@ definitions:
|
|||
example: bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y
|
||||
type: string
|
||||
type: object
|
||||
util.NotFoundError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
util.ServerError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
util.UnauthorizedError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
util.ValidationError:
|
||||
properties:
|
||||
error:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
externalDocs:
|
||||
description: OpenAPI
|
||||
url: https://swagger.io/resources/open-api/
|
||||
|
@ -326,11 +326,11 @@ paths:
|
|||
"200":
|
||||
description: Challenge ID
|
||||
schema:
|
||||
$ref: '#/definitions/api.AuthDisplayCodeResponse'
|
||||
$ref: '#/definitions/auth.AuthDisplayCodeResponse'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Open a modal window with a code in Anytype Desktop app
|
||||
tags:
|
||||
- auth
|
||||
|
@ -355,15 +355,15 @@ paths:
|
|||
"200":
|
||||
description: Authentication token
|
||||
schema:
|
||||
$ref: '#/definitions/api.AuthTokenResponse'
|
||||
$ref: '#/definitions/auth.AuthTokenResponse'
|
||||
"400":
|
||||
description: Invalid input
|
||||
schema:
|
||||
$ref: '#/definitions/api.ValidationError'
|
||||
$ref: '#/definitions/util.ValidationError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve an authentication token using a code
|
||||
tags:
|
||||
- auth
|
||||
|
@ -376,7 +376,7 @@ paths:
|
|||
in: query
|
||||
name: query
|
||||
type: string
|
||||
- description: Specify object type for search
|
||||
- description: Specify object.Object type for search
|
||||
in: query
|
||||
name: object_type
|
||||
type: string
|
||||
|
@ -398,17 +398,21 @@ paths:
|
|||
schema:
|
||||
additionalProperties:
|
||||
items:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
type: array
|
||||
type: object
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Search and retrieve objects across all the spaces
|
||||
tags:
|
||||
- search
|
||||
|
@ -437,15 +441,15 @@ paths:
|
|||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve a list of spaces
|
||||
tags:
|
||||
- spaces
|
||||
|
@ -469,11 +473,11 @@ paths:
|
|||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Create a new Space
|
||||
tags:
|
||||
- spaces
|
||||
|
@ -507,15 +511,15 @@ paths:
|
|||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve a list of members for the specified Space
|
||||
tags:
|
||||
- spaces
|
||||
|
@ -546,20 +550,20 @@ paths:
|
|||
description: List of object types
|
||||
schema:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/api.ObjectType'
|
||||
$ref: '#/definitions/object.ObjectType'
|
||||
type: object
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve object types in a specific space
|
||||
tags:
|
||||
- types_and_templates
|
||||
|
@ -596,21 +600,21 @@ paths:
|
|||
schema:
|
||||
additionalProperties:
|
||||
items:
|
||||
$ref: '#/definitions/api.ObjectTemplate'
|
||||
$ref: '#/definitions/object.ObjectTemplate'
|
||||
type: array
|
||||
type: object
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve a list of templates for a specific object type in a space
|
||||
tags:
|
||||
- types_and_templates
|
||||
|
@ -642,21 +646,21 @@ paths:
|
|||
schema:
|
||||
additionalProperties:
|
||||
items:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
type: array
|
||||
type: object
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve objects in a specific space
|
||||
tags:
|
||||
- space_objects
|
||||
|
@ -683,15 +687,15 @@ paths:
|
|||
"200":
|
||||
description: The created object
|
||||
schema:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Create a new object in a specific space
|
||||
tags:
|
||||
- space_objects
|
||||
|
@ -716,19 +720,19 @@ paths:
|
|||
"200":
|
||||
description: The requested object
|
||||
schema:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Retrieve a specific object in a space
|
||||
tags:
|
||||
- space_objects
|
||||
|
@ -751,26 +755,26 @@ paths:
|
|||
name: object
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The updated object
|
||||
schema:
|
||||
$ref: '#/definitions/api.Object'
|
||||
$ref: '#/definitions/object.Object'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.UnauthorizedError'
|
||||
$ref: '#/definitions/util.UnauthorizedError'
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: '#/definitions/api.NotFoundError'
|
||||
$ref: '#/definitions/util.NotFoundError'
|
||||
"502":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ServerError'
|
||||
$ref: '#/definitions/util.ServerError'
|
||||
summary: Update an existing object in a specific space
|
||||
tags:
|
||||
- space_objects
|
||||
|
|
|
@ -1,631 +1 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/utils"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
type CreateObjectRequest struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
TemplateId string `json:"template_id"`
|
||||
ObjectTypeUniqueKey string `json:"object_type_unique_key"`
|
||||
WithChat bool `json:"with_chat"`
|
||||
}
|
||||
|
||||
type AddMessageRequest struct {
|
||||
Text string `json:"text"`
|
||||
Style string `json:"style"`
|
||||
}
|
||||
|
||||
// authDisplayCodeHandler generates a new challenge and returns the challenge ID
|
||||
//
|
||||
// @Summary Open a modal window with a code in Anytype Desktop app
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} AuthDisplayCodeResponse "Challenge ID"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /auth/displayCode [post]
|
||||
func (a *ApiServer) authDisplayCodeHandler(c *gin.Context) {
|
||||
resp := a.mw.AccountLocalLinkNewChallenge(c.Request.Context(), &pb.RpcAccountLocalLinkNewChallengeRequest{AppName: "api-test"})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkNewChallengeResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to generate a new challenge."})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthDisplayCodeResponse{ChallengeId: resp.ChallengeId})
|
||||
}
|
||||
|
||||
// authTokenHandler retrieves an authentication token using a code and challenge ID
|
||||
//
|
||||
// @Summary Retrieve an authentication token using a code
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "The code retrieved from Anytype Desktop app"
|
||||
// @Param challenge_id query string true "The challenge ID"
|
||||
// @Success 200 {object} AuthTokenResponse "Authentication token"
|
||||
// @Failure 400 {object} ValidationError "Invalid input"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /auth/token [get]
|
||||
func (a *ApiServer) authTokenHandler(c *gin.Context) {
|
||||
if c.Query("challenge_id") == "" || c.Query("code") == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"})
|
||||
return
|
||||
}
|
||||
|
||||
// Call AccountLocalLinkSolveChallenge to retrieve session token and app key
|
||||
resp := a.mw.AccountLocalLinkSolveChallenge(c.Request.Context(), &pb.RpcAccountLocalLinkSolveChallengeRequest{
|
||||
ChallengeId: c.Query("challenge_id"),
|
||||
Answer: c.Query("code"),
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to authenticate user."})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthTokenResponse{
|
||||
SessionToken: resp.SessionToken,
|
||||
AppKey: resp.AppKey,
|
||||
})
|
||||
}
|
||||
|
||||
// getObjectsHandler retrieves objects in a specific space
|
||||
//
|
||||
// @Summary Retrieve objects in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]Object "List of objects"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [get]
|
||||
func (a *ApiServer) getObjectsHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
resp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{"id", "name", "type", "layout", "iconEmoji", "iconImage"},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of objects."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No objects found."})
|
||||
return
|
||||
}
|
||||
|
||||
paginatedObjects, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
objects := make([]Object, 0, len(paginatedObjects))
|
||||
|
||||
for _, record := range paginatedObjects {
|
||||
icon := utils.GetIconFromEmojiOrImage(a.accountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
objectTypeName, statusCode, errorMessage := a.resolveTypeToName(spaceId, record.Fields["type"].GetStringValue())
|
||||
if statusCode != http.StatusOK {
|
||||
c.JSON(statusCode, gin.H{"message": errorMessage})
|
||||
return
|
||||
}
|
||||
|
||||
objectShowResp := a.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: record.Fields["id"].GetStringValue(),
|
||||
})
|
||||
|
||||
object := Object{
|
||||
// TODO fix type inconsistency
|
||||
Type: model.ObjectTypeLayout_name[int32(record.Fields["layout"].GetNumberValue())],
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: icon,
|
||||
ObjectType: objectTypeName,
|
||||
SpaceId: spaceId,
|
||||
RootId: objectShowResp.ObjectView.RootId,
|
||||
Blocks: a.getBlocks(objectShowResp),
|
||||
Details: a.getDetails(objectShowResp),
|
||||
}
|
||||
|
||||
objects = append(objects, object)
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, len(resp.Records), offset, limit, hasMore)
|
||||
}
|
||||
|
||||
// getObjectHandler retrieves a specific object in a space
|
||||
//
|
||||
// @Summary Retrieve a specific object in a space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object_id path string true "The ID of the object"
|
||||
// @Success 200 {object} Object "The requested object"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [get]
|
||||
func (a *ApiServer) getObjectHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
|
||||
resp := a.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: objectId,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "Object not found", "space_id": spaceId, "object_id": objectId})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve object."})
|
||||
return
|
||||
}
|
||||
|
||||
objectTypeName, statusCode, errorMessage := a.resolveTypeToName(spaceId, resp.ObjectView.Details[0].Details.Fields["type"].GetStringValue())
|
||||
if statusCode != http.StatusOK {
|
||||
c.JSON(statusCode, gin.H{"message": errorMessage})
|
||||
return
|
||||
}
|
||||
|
||||
object := Object{
|
||||
Type: "object",
|
||||
Id: objectId,
|
||||
Name: resp.ObjectView.Details[0].Details.Fields["name"].GetStringValue(),
|
||||
Icon: resp.ObjectView.Details[0].Details.Fields["iconEmoji"].GetStringValue(),
|
||||
ObjectType: objectTypeName,
|
||||
RootId: resp.ObjectView.RootId,
|
||||
Blocks: a.getBlocks(resp),
|
||||
Details: a.getDetails(resp),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"object": object})
|
||||
}
|
||||
|
||||
// createObjectHandler creates a new object in a specific space
|
||||
//
|
||||
// @Summary Create a new object in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object body map[string]string true "Object details (e.g., name)"
|
||||
// @Success 200 {object} Object "The created object"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [post]
|
||||
func (a *ApiServer) createObjectHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
|
||||
request := CreateObjectRequest{}
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
resp := a.mw.ObjectCreate(c.Request.Context(), &pb.RpcObjectCreateRequest{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
"name": {Kind: &types.Value_StringValue{StringValue: request.Name}},
|
||||
"iconEmoji": {Kind: &types.Value_StringValue{StringValue: request.Icon}},
|
||||
},
|
||||
},
|
||||
TemplateId: request.TemplateId,
|
||||
SpaceId: spaceId,
|
||||
ObjectTypeUniqueKey: request.ObjectTypeUniqueKey,
|
||||
WithChat: request.WithChat,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create a new object."})
|
||||
return
|
||||
}
|
||||
|
||||
object := Object{
|
||||
Type: "object",
|
||||
Id: resp.ObjectId,
|
||||
Name: resp.Details.Fields["name"].GetStringValue(),
|
||||
Icon: resp.Details.Fields["iconEmoji"].GetStringValue(),
|
||||
ObjectType: request.ObjectTypeUniqueKey,
|
||||
SpaceId: resp.Details.Fields["spaceId"].GetStringValue(),
|
||||
// TODO populate other fields
|
||||
// RootId: resp.RootId,
|
||||
// Blocks: []Block{},
|
||||
// Details: []Detail{},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"object": object})
|
||||
}
|
||||
|
||||
// updateObjectHandler updates an existing object in a specific space
|
||||
//
|
||||
// @Summary Update an existing object in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object_id path string true "The ID of the object"
|
||||
// @Param object body Object true "The updated object details"
|
||||
// @Success 200 {object} Object "The updated object"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [put]
|
||||
func (a *ApiServer) updateObjectHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
// TODO: Implement logic to update an existing object
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet", "space_id": spaceId, "object_id": objectId})
|
||||
}
|
||||
|
||||
// getObjectTypesHandler retrieves object types in a specific space
|
||||
//
|
||||
// @Summary Retrieve object types in a specific space
|
||||
// @Tags types_and_templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string]ObjectType "List of object types"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objectTypes [get]
|
||||
func (a *ApiServer) getObjectTypesHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
resp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: "name",
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
},
|
||||
},
|
||||
Keys: []string{"id", "uniqueKey", "name", "iconEmoji"},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve object types."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No object types found."})
|
||||
return
|
||||
}
|
||||
|
||||
paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
objectTypes := make([]ObjectType, 0, len(paginatedTypes))
|
||||
|
||||
for _, record := range paginatedTypes {
|
||||
objectTypes = append(objectTypes, ObjectType{
|
||||
Type: "object_type",
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
UniqueKey: record.Fields["uniqueKey"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: record.Fields["iconEmoji"].GetStringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objectTypes, len(resp.Records), offset, limit, hasMore)
|
||||
}
|
||||
|
||||
// getObjectTypeTemplatesHandler retrieves a list of templates for a specific object type in a space
|
||||
//
|
||||
// @Summary Retrieve a list of templates for a specific object type in a space
|
||||
// @Tags types_and_templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param typeId path string true "The ID of the object type"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]ObjectTemplate "List of templates"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objectTypes/{typeId}/templates [get]
|
||||
func (a *ApiServer) getObjectTypeTemplatesHandler(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
typeId := c.Param("typeId")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
// First, determine the type ID of "ot-template" in the space
|
||||
templateTypeIdResp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String("ot-template"),
|
||||
},
|
||||
},
|
||||
Keys: []string{"id"},
|
||||
})
|
||||
|
||||
if templateTypeIdResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template type."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(templateTypeIdResp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "Template type not found."})
|
||||
return
|
||||
}
|
||||
|
||||
templateTypeId := templateTypeIdResp.Records[0].Fields["id"].GetStringValue()
|
||||
|
||||
// Then, search all objects of the template type and filter by the target object type
|
||||
templateObjectsResp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(templateTypeId),
|
||||
},
|
||||
},
|
||||
Keys: []string{"id", "targetObjectType", "name", "iconEmoji"},
|
||||
})
|
||||
|
||||
if templateObjectsResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template objects."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(templateObjectsResp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No templates found."})
|
||||
return
|
||||
}
|
||||
|
||||
templateIds := make([]string, 0)
|
||||
for _, record := range templateObjectsResp.Records {
|
||||
if record.Fields["targetObjectType"].GetStringValue() == typeId {
|
||||
templateIds = append(templateIds, record.Fields["id"].GetStringValue())
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, open each template and populate the response
|
||||
paginatedTemplates, hasMore := pagination.Paginate(templateIds, offset, limit)
|
||||
templates := make([]ObjectTemplate, 0, len(paginatedTemplates))
|
||||
|
||||
for _, templateId := range paginatedTemplates {
|
||||
templateResp := a.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: templateId,
|
||||
})
|
||||
|
||||
if templateResp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template."})
|
||||
return
|
||||
}
|
||||
|
||||
templates = append(templates, ObjectTemplate{
|
||||
Type: "object_template",
|
||||
Id: templateId,
|
||||
Name: templateResp.ObjectView.Details[0].Details.Fields["name"].GetStringValue(),
|
||||
Icon: templateResp.ObjectView.Details[0].Details.Fields["iconEmoji"].GetStringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, templates, len(templateIds), offset, limit, hasMore)
|
||||
}
|
||||
|
||||
// searchHandler searches and retrieves objects across all the spaces
|
||||
//
|
||||
// @Summary Search and retrieve objects across all the spaces
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string false "The search term to filter objects by name"
|
||||
// @Param object_type query string false "Specify object type for search"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]Object "List of objects"
|
||||
// @Failure 403 {object} UnauthorizedError "Unauthorized"
|
||||
// @Failure 502 {object} ServerError "Internal server error"
|
||||
// @Router /search [get]
|
||||
func (a *ApiServer) searchHandler(c *gin.Context) {
|
||||
searchQuery := c.Query("query")
|
||||
objectType := c.Query("object_type")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
// First, call ObjectSearch for all objects of type spaceView
|
||||
spaceResp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: a.accountInfo.TechSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeySpaceRemoteStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
},
|
||||
Keys: []string{"targetSpaceId"},
|
||||
})
|
||||
|
||||
if spaceResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of spaces."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(spaceResp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No spaces found."})
|
||||
return
|
||||
}
|
||||
|
||||
// Then, get objects from each space that match the search parameters
|
||||
var filters = []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
}
|
||||
|
||||
if searchQuery != "" {
|
||||
// TODO also include snippet for notes
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
})
|
||||
}
|
||||
|
||||
if objectType != "" {
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(objectType),
|
||||
})
|
||||
}
|
||||
|
||||
searchResults := make([]Object, 0)
|
||||
for _, spaceRecord := range spaceResp.Records {
|
||||
spaceId := spaceRecord.Fields["targetSpaceId"].GetStringValue()
|
||||
objectResp := a.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: filters,
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{"id", "name", "type", "layout", "iconEmoji", "iconImage"},
|
||||
// TODO split limit between spaces
|
||||
// Limit: paginationLimitPerSpace,
|
||||
// FullText: searchTerm,
|
||||
})
|
||||
|
||||
if objectResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of objects."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(objectResp.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range objectResp.Records {
|
||||
icon := utils.GetIconFromEmojiOrImage(a.accountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
objectTypeName, statusCode, errorMessage := a.resolveTypeToName(spaceId, record.Fields["type"].GetStringValue())
|
||||
if statusCode != http.StatusOK {
|
||||
c.JSON(statusCode, gin.H{"message": errorMessage})
|
||||
return
|
||||
}
|
||||
|
||||
objectShowResp := a.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: record.Fields["id"].GetStringValue(),
|
||||
})
|
||||
|
||||
searchResults = append(searchResults, Object{
|
||||
Type: model.ObjectTypeLayout_name[int32(record.Fields["layout"].GetNumberValue())],
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: icon,
|
||||
ObjectType: objectTypeName,
|
||||
SpaceId: spaceId,
|
||||
RootId: objectShowResp.ObjectView.RootId,
|
||||
Blocks: a.getBlocks(objectShowResp),
|
||||
Details: a.getDetails(objectShowResp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// sort after lastModifiedDate to achieve descending sort order across all spaces
|
||||
sort.Slice(searchResults, func(i, j int) bool {
|
||||
return searchResults[i].Details[0].Details["lastModifiedDate"].(float64) > searchResults[j].Details[0].Details["lastModifiedDate"].(float64)
|
||||
})
|
||||
|
||||
// TODO: solve global pagination vs per space pagination
|
||||
paginatedResults, hasMore := pagination.Paginate(searchResults, offset, limit)
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, paginatedResults, len(searchResults), offset, limit, hasMore)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,10 @@ import (
|
|||
|
||||
"github.com/webstradev/gin-pagination/v2/pkg/pagination"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/auth"
|
||||
_ "github.com/anyproto/anytype-heart/cmd/api/docs"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/object"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/search"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/space"
|
||||
"github.com/anyproto/anytype-heart/core"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
|
@ -30,8 +33,11 @@ type ApiServer struct {
|
|||
router *gin.Engine
|
||||
server *http.Server
|
||||
|
||||
accountInfo *model.AccountInfo
|
||||
spaceService *space.SpaceService
|
||||
accountInfo *model.AccountInfo
|
||||
authService *auth.AuthService
|
||||
objectService *object.ObjectService
|
||||
spaceService *space.SpaceService
|
||||
searchService *search.SearchService
|
||||
}
|
||||
|
||||
// TODO: User represents an authenticated user with permissions
|
||||
|
@ -42,10 +48,13 @@ type User struct {
|
|||
|
||||
func newApiServer(mw service.ClientCommandsServer, mwInternal core.MiddlewareInternal) *ApiServer {
|
||||
a := &ApiServer{
|
||||
mw: mw,
|
||||
mwInternal: mwInternal,
|
||||
router: gin.Default(),
|
||||
spaceService: space.NewService(mw),
|
||||
mw: mw,
|
||||
mwInternal: mwInternal,
|
||||
router: gin.Default(),
|
||||
authService: auth.NewService(mw),
|
||||
objectService: object.NewService(mw),
|
||||
spaceService: space.NewService(mw),
|
||||
searchService: search.NewService(mw),
|
||||
}
|
||||
|
||||
a.server = &http.Server{
|
||||
|
@ -91,10 +100,10 @@ func RunApiServer(ctx context.Context, mw service.ClientCommandsServer, mwIntern
|
|||
a.router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Unprotected routes
|
||||
auth := a.router.Group("/v1/auth")
|
||||
authRouter := a.router.Group("/v1/auth")
|
||||
{
|
||||
auth.POST("/displayCode", a.authDisplayCodeHandler)
|
||||
auth.GET("/token", a.authTokenHandler)
|
||||
authRouter.POST("/displayCode", auth.AuthDisplayCodeHandler(a.authService))
|
||||
authRouter.GET("/token", auth.AuthTokenHandler(a.authService))
|
||||
}
|
||||
|
||||
// Read-only routes
|
||||
|
@ -104,11 +113,11 @@ func RunApiServer(ctx context.Context, mw service.ClientCommandsServer, mwIntern
|
|||
{
|
||||
readOnly.GET("/spaces", paginator, space.GetSpacesHandler(a.spaceService))
|
||||
readOnly.GET("/spaces/:space_id/members", paginator, space.GetMembersHandler(a.spaceService))
|
||||
readOnly.GET("/spaces/:space_id/objects", paginator, a.getObjectsHandler)
|
||||
readOnly.GET("/spaces/:space_id/objects/:object_id", a.getObjectHandler)
|
||||
readOnly.GET("/spaces/:space_id/objectTypes", paginator, a.getObjectTypesHandler)
|
||||
readOnly.GET("/spaces/:space_id/objectTypes/:typeId/templates", paginator, a.getObjectTypeTemplatesHandler)
|
||||
readOnly.GET("/search", paginator, a.searchHandler)
|
||||
readOnly.GET("/spaces/:space_id/objects", paginator, object.GetObjectsHandler(a.objectService))
|
||||
readOnly.GET("/spaces/:space_id/objects/:object_id", object.GetObjectHandler(a.objectService))
|
||||
readOnly.GET("/spaces/:space_id/objectTypes", paginator, object.GetObjectTypesHandler(a.objectService))
|
||||
readOnly.GET("/spaces/:space_id/objectTypes/:typeId/templates", paginator, object.GetObjectTypeTemplatesHandler(a.objectService))
|
||||
readOnly.GET("/search", paginator, search.SearchHandler(a.searchService))
|
||||
}
|
||||
|
||||
// Read-write routes
|
||||
|
@ -117,8 +126,8 @@ func RunApiServer(ctx context.Context, mw service.ClientCommandsServer, mwIntern
|
|||
// readWrite.Use(a.PermissionMiddleware("read-write"))
|
||||
{
|
||||
// readWrite.POST("/spaces", a.createSpaceHandler)
|
||||
readWrite.POST("/spaces/:space_id/objects", a.createObjectHandler)
|
||||
readWrite.PUT("/spaces/:space_id/objects/:object_id", a.updateObjectHandler)
|
||||
readWrite.POST("/spaces/:space_id/objects", object.CreateObjectHandler(a.objectService))
|
||||
readWrite.PUT("/spaces/:space_id/objects/:object_id", object.UpdateObjectHandler(a.objectService))
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
|
|
|
@ -27,7 +27,9 @@ func (a *ApiServer) initAccountInfo() gin.HandlerFunc {
|
|||
}
|
||||
|
||||
a.accountInfo = accInfo
|
||||
a.objectService.AccountInfo = accInfo
|
||||
a.spaceService.AccountInfo = accInfo
|
||||
a.searchService.AccountInfo = accInfo
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
|
424
cmd/api/object/handler.go
Normal file
424
cmd/api/object/handler.go
Normal file
|
@ -0,0 +1,424 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
type CreateObjectRequest struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
TemplateId string `json:"template_id"`
|
||||
ObjectTypeUniqueKey string `json:"object_type_unique_key"`
|
||||
WithChat bool `json:"with_chat"`
|
||||
}
|
||||
|
||||
// GetObjectsHandler retrieves objects in a specific space
|
||||
//
|
||||
// @Summary Retrieve objects in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]Object "List of objects"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [get]
|
||||
func GetObjectsHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
resp := s.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{"id", "name", "type", "layout", "iconEmoji", "iconImage"},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of objects."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No objects found."})
|
||||
return
|
||||
}
|
||||
|
||||
paginatedObjects, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
objects := make([]Object, 0, len(paginatedObjects))
|
||||
|
||||
for _, record := range paginatedObjects {
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, record.Fields["type"].GetStringValue())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to resolve object type name."})
|
||||
return
|
||||
}
|
||||
|
||||
objectShowResp := s.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: record.Fields["id"].GetStringValue(),
|
||||
})
|
||||
|
||||
object := Object{
|
||||
// TODO fix type inconsistency
|
||||
Type: model.ObjectTypeLayout_name[int32(record.Fields["layout"].GetNumberValue())],
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: icon,
|
||||
ObjectType: objectTypeName,
|
||||
SpaceId: spaceId,
|
||||
RootId: objectShowResp.ObjectView.RootId,
|
||||
Blocks: s.GetBlocks(objectShowResp),
|
||||
Details: s.GetDetails(objectShowResp),
|
||||
}
|
||||
|
||||
objects = append(objects, object)
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, len(resp.Records), offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectHandler retrieves a specific object in a space
|
||||
//
|
||||
// @Summary Retrieve a specific object in a space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object_id path string true "The ID of the object"
|
||||
// @Success 200 {object} Object "The requested object"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [get]
|
||||
func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
|
||||
resp := s.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: objectId,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "Object not found", "space_id": spaceId, "object_id": objectId})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve object."})
|
||||
return
|
||||
}
|
||||
|
||||
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, resp.ObjectView.Details[0].Details.Fields["type"].GetStringValue())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to resolve object type name."})
|
||||
return
|
||||
}
|
||||
|
||||
object := Object{
|
||||
Type: "object",
|
||||
Id: objectId,
|
||||
Name: resp.ObjectView.Details[0].Details.Fields["name"].GetStringValue(),
|
||||
Icon: resp.ObjectView.Details[0].Details.Fields["iconEmoji"].GetStringValue(),
|
||||
ObjectType: objectTypeName,
|
||||
RootId: resp.ObjectView.RootId,
|
||||
Blocks: s.GetBlocks(resp),
|
||||
Details: s.GetDetails(resp),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"object": object})
|
||||
}
|
||||
}
|
||||
|
||||
// CreateObjectHandler creates a new object in a specific space
|
||||
//
|
||||
// @Summary Create a new object in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object body map[string]string true "Object details (e.g., name)"
|
||||
// @Success 200 {object} Object "The created object"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [post]
|
||||
func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
|
||||
request := CreateObjectRequest{}
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.mw.ObjectCreate(c.Request.Context(), &pb.RpcObjectCreateRequest{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
"name": pbtypes.String(request.Name),
|
||||
"iconEmoji": pbtypes.String(request.Icon),
|
||||
},
|
||||
},
|
||||
TemplateId: request.TemplateId,
|
||||
SpaceId: spaceId,
|
||||
ObjectTypeUniqueKey: request.ObjectTypeUniqueKey,
|
||||
WithChat: request.WithChat,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create a new object."})
|
||||
return
|
||||
}
|
||||
|
||||
object := Object{
|
||||
Type: "object",
|
||||
Id: resp.ObjectId,
|
||||
Name: resp.Details.Fields["name"].GetStringValue(),
|
||||
Icon: resp.Details.Fields["iconEmoji"].GetStringValue(),
|
||||
ObjectType: request.ObjectTypeUniqueKey,
|
||||
SpaceId: resp.Details.Fields["spaceId"].GetStringValue(),
|
||||
// TODO populate other fields
|
||||
// RootId: resp.RootId,
|
||||
// Blocks: []Block{},
|
||||
// Details: []Detail{},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"object": object})
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateObjectHandler updates an existing object in a specific space
|
||||
//
|
||||
// @Summary Update an existing object in a specific space
|
||||
// @Tags space_objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param object_id path string true "The ID of the object"
|
||||
// @Param object body Object true "The updated object details"
|
||||
// @Success 200 {object} Object "The updated object"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [put]
|
||||
func UpdateObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
// TODO: Implement logic to update an existing object
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet", "space_id": spaceId, "object_id": objectId})
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectTypesHandler retrieves object types in a specific space
|
||||
//
|
||||
// @Summary Retrieve object types in a specific space
|
||||
// @Tags types_and_templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string]ObjectType "List of object types"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objectTypes [get]
|
||||
func GetObjectTypesHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
resp := s.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: "name",
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
},
|
||||
},
|
||||
Keys: []string{"id", "uniqueKey", "name", "iconEmoji"},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve object types."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No object types found."})
|
||||
return
|
||||
}
|
||||
|
||||
paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
objectTypes := make([]ObjectType, 0, len(paginatedTypes))
|
||||
|
||||
for _, record := range paginatedTypes {
|
||||
objectTypes = append(objectTypes, ObjectType{
|
||||
Type: "object_type",
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
UniqueKey: record.Fields["uniqueKey"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: record.Fields["iconEmoji"].GetStringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objectTypes, len(resp.Records), offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectTypeTemplatesHandler retrieves a list of templates for a specific object type in a space
|
||||
//
|
||||
// @Summary Retrieve a list of templates for a specific object type in a space
|
||||
// @Tags types_and_templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "The ID of the space"
|
||||
// @Param typeId path string true "The ID of the object type"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]ObjectTemplate "List of templates"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objectTypes/{typeId}/templates [get]
|
||||
func GetObjectTypeTemplatesHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
typeId := c.Param("typeId")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
// First, determine the type ID of "ot-template" in the space
|
||||
templateTypeIdResp := s.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String("ot-template"),
|
||||
},
|
||||
},
|
||||
Keys: []string{"id"},
|
||||
})
|
||||
|
||||
if templateTypeIdResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template type."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(templateTypeIdResp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "Template type not found."})
|
||||
return
|
||||
}
|
||||
|
||||
templateTypeId := templateTypeIdResp.Records[0].Fields["id"].GetStringValue()
|
||||
|
||||
// Then, search all objects of the template type and filter by the target object type
|
||||
templateObjectsResp := s.mw.ObjectSearch(c.Request.Context(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(templateTypeId),
|
||||
},
|
||||
},
|
||||
Keys: []string{"id", "targetObjectType", "name", "iconEmoji"},
|
||||
})
|
||||
|
||||
if templateObjectsResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template objects."})
|
||||
return
|
||||
}
|
||||
|
||||
if len(templateObjectsResp.Records) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No templates found."})
|
||||
return
|
||||
}
|
||||
|
||||
templateIds := make([]string, 0)
|
||||
for _, record := range templateObjectsResp.Records {
|
||||
if record.Fields["targetObjectType"].GetStringValue() == typeId {
|
||||
templateIds = append(templateIds, record.Fields["id"].GetStringValue())
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, open each template and populate the response
|
||||
paginatedTemplates, hasMore := pagination.Paginate(templateIds, offset, limit)
|
||||
templates := make([]ObjectTemplate, 0, len(paginatedTemplates))
|
||||
|
||||
for _, templateId := range paginatedTemplates {
|
||||
templateResp := s.mw.ObjectShow(c.Request.Context(), &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: templateId,
|
||||
})
|
||||
|
||||
if templateResp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve template."})
|
||||
return
|
||||
}
|
||||
|
||||
templates = append(templates, ObjectTemplate{
|
||||
Type: "object_template",
|
||||
Id: templateId,
|
||||
Name: templateResp.ObjectView.Details[0].Details.Fields["name"].GetStringValue(),
|
||||
Icon: templateResp.ObjectView.Details[0].Details.Fields["iconEmoji"].GetStringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, templates, len(templateIds), offset, limit, hasMore)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,4 @@
|
|||
package api
|
||||
|
||||
type AuthDisplayCodeResponse struct {
|
||||
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"`
|
||||
}
|
||||
|
||||
type AuthTokenResponse struct {
|
||||
SessionToken string `json:"session_token" example:""`
|
||||
AppKey string `json:"app_key" example:""`
|
||||
}
|
||||
package object
|
||||
|
||||
type Object struct {
|
||||
Type string `json:"type" example:"object"`
|
||||
|
@ -76,27 +67,3 @@ type ObjectTemplate struct {
|
|||
Name string `json:"name" example:"Object Template Name"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
}
|
||||
|
||||
type ServerError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type UnauthorizedError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
|
@ -1,50 +1,119 @@
|
|||
package api
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/utils"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
// resolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type"
|
||||
func (a *ApiServer) resolveTypeToName(spaceId string, typeId string) (typeName string, statusCode int, errorMessage string) {
|
||||
// Can't look up preinstalled types based on relation key, therefore need to use unique key
|
||||
relKey := bundle.RelationKeyId.String()
|
||||
if strings.Contains(typeId, "ot-") {
|
||||
relKey = bundle.RelationKeyUniqueKey.String()
|
||||
}
|
||||
var (
|
||||
ErrFailedGenerateChallenge = errors.New("failed to generate a new challenge")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrorFailedAuthenticate = errors.New("failed to authenticate user")
|
||||
)
|
||||
|
||||
// Call ObjectSearch for object of specified type and return the name
|
||||
resp := a.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: relKey,
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(typeId),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return "", http.StatusInternalServerError, "Failed to search for type."
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
return "", http.StatusNotFound, "Type not found."
|
||||
}
|
||||
|
||||
return resp.Records[0].Fields["name"].GetStringValue(), http.StatusOK, ""
|
||||
type Service interface {
|
||||
ListObjects() ([]Object, error)
|
||||
GetObject(id string) (Object, error)
|
||||
CreateObject(obj Object) (Object, error)
|
||||
UpdateObject(obj Object) (Object, error)
|
||||
ListTypes() ([]ObjectType, error)
|
||||
ListTemplates() ([]ObjectTemplate, error)
|
||||
}
|
||||
|
||||
// getBlocks returns the blocks of the object
|
||||
func (a *ApiServer) getBlocks(resp *pb.RpcObjectShowResponse) []Block {
|
||||
type ObjectService struct {
|
||||
mw service.ClientCommandsServer
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *ObjectService {
|
||||
return &ObjectService{mw: mw}
|
||||
}
|
||||
|
||||
func (s *ObjectService) ListObjects() ([]Object, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *ObjectService) GetObject(id string) (Object, error) {
|
||||
// TODO
|
||||
return Object{}, nil
|
||||
}
|
||||
|
||||
func (s *ObjectService) CreateObject(obj Object) (Object, error) {
|
||||
// TODO
|
||||
return Object{}, nil
|
||||
}
|
||||
|
||||
func (s *ObjectService) UpdateObject(obj Object) (Object, error) {
|
||||
// TODO
|
||||
return Object{}, nil
|
||||
}
|
||||
|
||||
func (s *ObjectService) ListTypes() ([]ObjectType, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *ObjectService) ListTemplates() ([]ObjectTemplate, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetDetails returns the details of the object
|
||||
func (s *ObjectService) GetDetails(resp *pb.RpcObjectShowResponse) []Detail {
|
||||
return []Detail{
|
||||
{
|
||||
Id: "lastModifiedDate",
|
||||
Details: map[string]interface{}{
|
||||
"lastModifiedDate": resp.ObjectView.Details[0].Details.Fields["lastModifiedDate"].GetNumberValue(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "createdDate",
|
||||
Details: map[string]interface{}{
|
||||
"createdDate": resp.ObjectView.Details[0].Details.Fields["createdDate"].GetNumberValue(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "tags",
|
||||
Details: map[string]interface{}{
|
||||
"tags": s.getTags(resp),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getTags returns the list of tags from the object details
|
||||
func (s *ObjectService) getTags(resp *pb.RpcObjectShowResponse) []Tag {
|
||||
tags := []Tag{}
|
||||
|
||||
tagField, ok := resp.ObjectView.Details[0].Details.Fields["tag"]
|
||||
if !ok {
|
||||
return tags
|
||||
}
|
||||
|
||||
for _, tagId := range tagField.GetListValue().Values {
|
||||
id := tagId.GetStringValue()
|
||||
for _, detail := range resp.ObjectView.Details {
|
||||
if detail.Id == id {
|
||||
tags = append(tags, Tag{
|
||||
Id: id,
|
||||
Name: detail.Details.Fields["name"].GetStringValue(),
|
||||
Color: detail.Details.Fields["relationOptionColor"].GetStringValue(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// GetBlocks returns the blocks of the object
|
||||
func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
|
||||
blocks := []Block{}
|
||||
|
||||
for _, block := range resp.ObjectView.Blocks {
|
||||
|
@ -58,7 +127,7 @@ func (a *ApiServer) getBlocks(resp *pb.RpcObjectShowResponse) []Block {
|
|||
Style: model.BlockContentTextStyle_name[int32(content.Text.Style)],
|
||||
Checked: content.Text.Checked,
|
||||
Color: content.Text.Color,
|
||||
Icon: utils.GetIconFromEmojiOrImage(a.accountInfo, content.Text.IconEmoji, content.Text.IconImage),
|
||||
Icon: util.GetIconFromEmojiOrImage(s.AccountInfo, content.Text.IconEmoji, content.Text.IconImage),
|
||||
}
|
||||
case *model.BlockContentOfFile:
|
||||
file = &File{
|
||||
|
@ -116,52 +185,3 @@ func mapVerticalAlign(align model.BlockVerticalAlign) string {
|
|||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// getDetails returns the details of the object
|
||||
func (a *ApiServer) getDetails(resp *pb.RpcObjectShowResponse) []Detail {
|
||||
return []Detail{
|
||||
{
|
||||
Id: "lastModifiedDate",
|
||||
Details: map[string]interface{}{
|
||||
"lastModifiedDate": resp.ObjectView.Details[0].Details.Fields["lastModifiedDate"].GetNumberValue(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "createdDate",
|
||||
Details: map[string]interface{}{
|
||||
"createdDate": resp.ObjectView.Details[0].Details.Fields["createdDate"].GetNumberValue(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "tags",
|
||||
Details: map[string]interface{}{
|
||||
"tags": a.getTags(resp),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getTags returns the list of tags from the object details
|
||||
func (a *ApiServer) getTags(resp *pb.RpcObjectShowResponse) []Tag {
|
||||
tags := []Tag{}
|
||||
|
||||
tagField, ok := resp.ObjectView.Details[0].Details.Fields["tag"]
|
||||
if !ok {
|
||||
return tags
|
||||
}
|
||||
|
||||
for _, tagId := range tagField.GetListValue().Values {
|
||||
id := tagId.GetStringValue()
|
||||
for _, detail := range resp.ObjectView.Details {
|
||||
if detail.Id == id {
|
||||
tags = append(tags, Tag{
|
||||
Id: id,
|
||||
Name: detail.Details.Fields["name"].GetStringValue(),
|
||||
Color: detail.Details.Fields["relationOptionColor"].GetStringValue(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
50
cmd/api/search/handler.go
Normal file
50
cmd/api/search/handler.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/space"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
)
|
||||
|
||||
// SearchHandler searches and retrieves objects across all the spaces
|
||||
//
|
||||
// @Summary Search and retrieve objects across all the spaces
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string false "The search term to filter objects by name"
|
||||
// @Param object_type query string false "Specify object.Object type for search"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]object.Object "List of objects"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /search [get]
|
||||
func SearchHandler(s *SearchService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
searchQuery := c.Query("query")
|
||||
objectType := c.Query("object_type")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
objects, total, hasMore, err := s.Search(c, searchQuery, objectType, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrNoObjectsFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
|
||||
util.ErrToCode(space.ErrNoSpacesFound, http.StatusNotFound),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
151
cmd/api/search/service.go
Normal file
151
cmd/api/search/service.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/object"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/space"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedSearchObjects = errors.New("failed to retrieve objects from space")
|
||||
ErrNoObjectsFound = errors.New("no objects found")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Search(ctx context.Context) ([]object.Object, error)
|
||||
}
|
||||
|
||||
type SearchService struct {
|
||||
mw service.ClientCommandsServer
|
||||
spaceService *space.SpaceService
|
||||
objectService *object.ObjectService
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *SearchService {
|
||||
return &SearchService{mw: mw, spaceService: space.NewService(mw), objectService: object.NewService(mw)}
|
||||
}
|
||||
|
||||
func (s *SearchService) Search(ctx context.Context, searchQuery string, objectType string, offset, limit int) (objects []object.Object, total int, hasMore bool, err error) {
|
||||
spaces, _, _, err := s.spaceService.ListSpaces(ctx, 0, 100)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
// Then, get objects from each space that match the search parameters
|
||||
var filters = []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
}
|
||||
|
||||
if searchQuery != "" {
|
||||
// TODO also include snippet for notes
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
})
|
||||
}
|
||||
|
||||
if objectType != "" {
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(objectType),
|
||||
})
|
||||
}
|
||||
|
||||
results := make([]object.Object, 0)
|
||||
for _, space := range spaces {
|
||||
spaceId := space.Id
|
||||
objResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: filters,
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{"id", "name", "type", "layout", "iconEmoji", "iconImage"},
|
||||
// TODO split limit between spaces
|
||||
// Limit: paginationLimitPerSpace,
|
||||
// FullText: searchTerm,
|
||||
})
|
||||
|
||||
if objResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedSearchObjects
|
||||
}
|
||||
|
||||
if len(objResp.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range objResp.Records {
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, record.Fields["type"].GetStringValue())
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
showResp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: record.Fields["id"].GetStringValue(),
|
||||
})
|
||||
|
||||
results = append(results, object.Object{
|
||||
Type: model.ObjectTypeLayout_name[int32(record.Fields["layout"].GetNumberValue())],
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
Name: record.Fields["name"].GetStringValue(),
|
||||
Icon: icon,
|
||||
ObjectType: objectTypeName,
|
||||
SpaceId: spaceId,
|
||||
RootId: showResp.ObjectView.RootId,
|
||||
Blocks: s.objectService.GetBlocks(showResp),
|
||||
Details: s.objectService.GetDetails(showResp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, 0, false, ErrNoObjectsFound
|
||||
}
|
||||
|
||||
// sort after lastModifiedDate to achieve descending sort order across all spaces
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Details[0].Details["lastModifiedDate"].(float64) > results[j].Details[0].Details["lastModifiedDate"].(float64)
|
||||
})
|
||||
|
||||
// TODO: solve global pagination vs per space pagination
|
||||
total = len(results)
|
||||
paginatedResults, hasMore := pagination.Paginate(results, offset, limit)
|
||||
return paginatedResults, total, hasMore, nil
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package space
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
)
|
||||
|
||||
// GetSpacesHandler retrieves a list of spaces
|
||||
|
@ -18,9 +18,9 @@ import (
|
|||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Space] "List of spaces"
|
||||
// @Failure 403 {object} api.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} api.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} api.ServerError "Internal server error"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces [get]
|
||||
func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
@ -28,20 +28,16 @@ func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
|
|||
limit := c.GetInt("limit")
|
||||
|
||||
spaces, total, hasMore, err := s.ListSpaces(c.Request.Context(), offset, limit)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrNoSpacesFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No spaces found."})
|
||||
return
|
||||
case errors.Is(err, ErrFailedListSpaces):
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of spaces."})
|
||||
return
|
||||
case errors.Is(err, ErrFailedOpenWorkspace):
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to open workspace."})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrNoSpacesFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedListSpaces, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, spaces, total, offset, limit, hasMore)
|
||||
|
@ -56,8 +52,8 @@ func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
|
|||
// @Produce json
|
||||
// @Param name body string true "Space Name"
|
||||
// @Success 200 {object} CreateSpaceResponse "Space created successfully"
|
||||
// @Failure 403 {object} api.UnauthorizedError "Unauthorized"
|
||||
// @Failure 502 {object} api.ServerError "Internal server error"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces [post]
|
||||
func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
@ -69,15 +65,14 @@ func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
|
|||
name := nameRequest.Name
|
||||
|
||||
space, err := s.CreateSpace(c.Request.Context(), name)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrFailedCreateSpace):
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to create space."})
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedCreateSpace, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CreateSpaceResponse{Space: space})
|
||||
|
@ -94,9 +89,9 @@ func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
|
|||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Member] "List of members"
|
||||
// @Failure 403 {object} api.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} api.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} api.ServerError "Internal server error"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/members [get]
|
||||
func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
@ -105,18 +100,15 @@ func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
|
|||
limit := c.GetInt("limit")
|
||||
|
||||
members, total, hasMore, err := s.ListMembers(c.Request.Context(), spaceId, offset, limit)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrNoMembersFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"message": "No members found."})
|
||||
return
|
||||
case errors.Is(err, ErrFailedListMembers):
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to retrieve list of members."})
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrNoMembersFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedListMembers, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, members, total, offset, limit, hasMore)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/utils"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
|
@ -92,7 +92,7 @@ func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (s
|
|||
|
||||
// TODO: name and icon are only returned here; fix that
|
||||
workspace.Name = record.Fields["name"].GetStringValue()
|
||||
workspace.Icon = utils.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
workspace.Icon = util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
|
||||
spaces = append(spaces, workspace)
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset i
|
|||
members = make([]Member, 0, len(paginatedMembers))
|
||||
|
||||
for _, record := range paginatedMembers {
|
||||
icon := utils.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields["iconEmoji"].GetStringValue(), record.Fields["iconImage"].GetStringValue())
|
||||
|
||||
member := Member{
|
||||
Type: "space_member",
|
||||
|
|
100
cmd/api/util/error.go
Normal file
100
cmd/api/util/error.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ServerError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type UnauthorizedError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type errCodeMapping struct {
|
||||
target error
|
||||
code int
|
||||
}
|
||||
|
||||
// ErrToCode just returns a mapping to pair a target error with a code
|
||||
func ErrToCode(target error, code int) errCodeMapping {
|
||||
return errCodeMapping{
|
||||
target: target,
|
||||
code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// MapErrorCode checks if err matches any “target” in the mappings,
|
||||
// returning the first matching code. If none match, returns 500.
|
||||
func MapErrorCode(err error, mappings ...errCodeMapping) int {
|
||||
if err == nil {
|
||||
return http.StatusOK
|
||||
}
|
||||
for _, m := range mappings {
|
||||
if errors.Is(err, m.target) {
|
||||
return m.code
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// CodeToAPIError returns an instance of the correct struct
|
||||
// for the given HTTP code, embedding the supplied message.
|
||||
func CodeToAPIError(code int, message string) any {
|
||||
switch code {
|
||||
case http.StatusNotFound:
|
||||
return NotFoundError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
return UnauthorizedError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
case http.StatusBadRequest:
|
||||
return ValidationError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
default:
|
||||
return ServerError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
72
cmd/api/util/util.go
Normal file
72
cmd/api/util/util.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedSearchType = errors.New("failed to search for type")
|
||||
ErrorTypeNotFound = errors.New("type not found")
|
||||
)
|
||||
|
||||
// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url
|
||||
func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string {
|
||||
if iconEmoji != "" {
|
||||
return iconEmoji
|
||||
}
|
||||
|
||||
if iconImage != "" {
|
||||
return GetGatewayURLForMedia(accountInfo, iconImage, true)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetGatewayURLForMedia returns the URL of file gateway for the media object with the given ID
|
||||
func GetGatewayURLForMedia(accountInfo *model.AccountInfo, objectId string, isIcon bool) string {
|
||||
widthParam := ""
|
||||
if isIcon {
|
||||
widthParam = "?width=100"
|
||||
}
|
||||
return fmt.Sprintf("%s/image/%s%s", accountInfo.GatewayUrl, objectId, widthParam)
|
||||
}
|
||||
|
||||
// ResolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type"
|
||||
func ResolveTypeToName(mw service.ClientCommandsServer, spaceId string, typeId string) (typeName string, err error) {
|
||||
// Can't look up preinstalled types based on relation key, therefore need to use unique key
|
||||
relKey := bundle.RelationKeyId.String()
|
||||
if strings.Contains(typeId, "ot-") {
|
||||
relKey = bundle.RelationKeyUniqueKey.String()
|
||||
}
|
||||
|
||||
// Call ObjectSearch for object of specified type and return the name
|
||||
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: relKey,
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(typeId),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return "", ErrFailedSearchType
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
return "", ErrorTypeNotFound
|
||||
}
|
||||
|
||||
return resp.Records[0].Fields["name"].GetStringValue(), nil
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url
|
||||
func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string {
|
||||
if iconEmoji != "" {
|
||||
return iconEmoji
|
||||
}
|
||||
|
||||
if iconImage != "" {
|
||||
return GetGatewayURLForMedia(accountInfo, iconImage, true)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetGatewayURLForMedia returns the URL of file gateway for the media object with the given ID
|
||||
func GetGatewayURLForMedia(accountInfo *model.AccountInfo, objectId string, isIcon bool) string {
|
||||
widthParam := ""
|
||||
if isIcon {
|
||||
widthParam = "?width=100"
|
||||
}
|
||||
return fmt.Sprintf("%s/image/%s%s", accountInfo.GatewayUrl, objectId, widthParam)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue