1
0
Fork 0
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:
Jannis Metrikat 2024-12-30 20:30:07 +01:00
parent c0f69df4b9
commit 5f7355f496
No known key found for this signature in database
GPG key ID: B223CAC5AAF85615
19 changed files with 1433 additions and 1150 deletions

69
cmd/api/auth/handler.go Normal file
View 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
View 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
View 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
}

View file

@ -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": {

View file

@ -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": {

View file

@ -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

View file

@ -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)
}

View file

@ -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

View file

@ -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
View 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)
}
}

View file

@ -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"`
}

View file

@ -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
View 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
View 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
}

View file

@ -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)

View file

@ -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
View 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
View 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
}

View file

@ -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)
}