1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-09 09:35:00 +09:00

GO-4459: Add object export endpoint

This commit is contained in:
Jannis Metrikat 2025-01-03 19:15:01 +01:00
parent 0358d34af7
commit f869fbf7b7
No known key found for this signature in database
GPG key ID: B223CAC5AAF85615
10 changed files with 365 additions and 27 deletions

View file

@ -736,6 +736,75 @@ const docTemplate = `{
}
}
}
},
"/spaces/{space_id}/objects/{object_id}/export/{format}": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"exports"
],
"summary": "Export an object",
"parameters": [
{
"type": "string",
"description": "Space ID",
"name": "space_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Object ID",
"name": "object_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Export format",
"name": "format",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Object exported successfully",
"schema": {
"$ref": "#/definitions/export.ObjectExportResponse"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/util.ValidationError"
}
},
"403": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/util.UnauthorizedError"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/util.NotFoundError"
}
},
"502": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/util.ServerError"
}
}
}
}
}
},
"definitions": {
@ -761,6 +830,14 @@ const docTemplate = `{
}
}
},
"export.ObjectExportResponse": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"object.Block": {
"type": "object",
"properties": {

View file

@ -730,6 +730,75 @@
}
}
}
},
"/spaces/{space_id}/objects/{object_id}/export/{format}": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"exports"
],
"summary": "Export an object",
"parameters": [
{
"type": "string",
"description": "Space ID",
"name": "space_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Object ID",
"name": "object_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Export format",
"name": "format",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Object exported successfully",
"schema": {
"$ref": "#/definitions/export.ObjectExportResponse"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/util.ValidationError"
}
},
"403": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/util.UnauthorizedError"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/util.NotFoundError"
}
},
"502": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/util.ServerError"
}
}
}
}
}
},
"definitions": {
@ -755,6 +824,14 @@
}
}
},
"export.ObjectExportResponse": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"object.Block": {
"type": "object",
"properties": {

View file

@ -15,6 +15,11 @@ definitions:
example: ""
type: string
type: object
export.ObjectExportResponse:
properties:
path:
type: string
type: object
object.Block:
properties:
align:
@ -803,6 +808,52 @@ paths:
summary: Update an existing object in a specific space
tags:
- space_objects
/spaces/{space_id}/objects/{object_id}/export/{format}:
post:
consumes:
- application/json
parameters:
- description: Space ID
in: path
name: space_id
required: true
type: string
- description: Object ID
in: path
name: object_id
required: true
type: string
- description: Export format
in: query
name: format
required: true
type: string
produces:
- application/json
responses:
"200":
description: Object exported successfully
schema:
$ref: '#/definitions/export.ObjectExportResponse'
"400":
description: Bad request
schema:
$ref: '#/definitions/util.ValidationError'
"403":
description: Unauthorized
schema:
$ref: '#/definitions/util.UnauthorizedError'
"404":
description: Resource not found
schema:
$ref: '#/definitions/util.NotFoundError'
"502":
description: Internal server error
schema:
$ref: '#/definitions/util.ServerError'
summary: Export an object
tags:
- exports
securityDefinitions:
BasicAuth:
type: basic

59
cmd/api/export/handler.go Normal file
View file

@ -0,0 +1,59 @@
package export
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/cmd/api/util"
)
// GetObjectExportHandler exports an object to the specified format
//
// @Summary Export an object
// @Tags exports
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Param format query string true "Export format"
// @Success 200 {object} ObjectExportResponse "Object exported successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @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}/export/{format} [post]
func GetObjectExportHandler(s *ExportService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
format := c.Query("format")
objectAsRequest := ObjectExportRequest{}
if err := c.ShouldBindJSON(&objectAsRequest); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, ErrBadInput.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
outputPath, err := s.GetObjectExport(c.Request.Context(), spaceId, objectId, format, objectAsRequest.Path)
code := util.MapErrorCode(err, util.ErrToCode(ErrFailedExportObjectAsMarkdown, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, ObjectExportResponse{Path: outputPath})
}
}
func GetSpaceExportHandler(s *ExportService) gin.HandlerFunc {
return func(c *gin.Context) {
// spaceId := c.Param("space_id")
// format := c.Query("format")
c.JSON(http.StatusNotImplemented, "Not implemented")
}
}

9
cmd/api/export/model.go Normal file
View file

@ -0,0 +1,9 @@
package export
type ObjectExportRequest struct {
Path string `json:"path"`
}
type ObjectExportResponse struct {
Path string `json:"path"`
}

61
cmd/api/export/service.go Normal file
View file

@ -0,0 +1,61 @@
package export
import (
"context"
"errors"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrFailedExportObjectAsMarkdown = errors.New("failed to export object as markdown")
ErrBadInput = errors.New("bad input")
)
type Service interface {
GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error)
}
type ExportService struct {
mw service.ClientCommandsServer
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer) *ExportService {
return &ExportService{mw: mw}
}
// GetObjectExport retrieves an object from a space and exports it as a specific format.
func (s *ExportService) GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error) {
resp := s.mw.ObjectListExport(ctx, &pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: path,
ObjectIds: []string{objectId},
Format: s.mapStringToFormat(format),
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
})
if resp.Error.Code != pb.RpcObjectListExportResponseError_NULL {
return "", ErrFailedExportObjectAsMarkdown
}
return resp.Path, nil
}
// mapStringToFormat maps a format string to an ExportFormat enum.
func (s *ExportService) mapStringToFormat(format string) model.ExportFormat {
switch format {
case "markdown":
return model.Export_Markdown
case "protobuf":
return model.Export_Protobuf
default:
return model.Export_Markdown
}
}

View file

@ -26,6 +26,7 @@ func (s *Server) initAccountInfo() gin.HandlerFunc {
return
}
s.exportService.AccountInfo = accInfo
s.objectService.AccountInfo = accInfo
s.spaceService.AccountInfo = accInfo
s.searchService.AccountInfo = accInfo

View file

@ -7,6 +7,7 @@ import (
"github.com/webstradev/gin-pagination/v2/pkg/pagination"
"github.com/anyproto/anytype-heart/cmd/api/auth"
"github.com/anyproto/anytype-heart/cmd/api/export"
"github.com/anyproto/anytype-heart/cmd/api/object"
"github.com/anyproto/anytype-heart/cmd/api/search"
"github.com/anyproto/anytype-heart/cmd/api/space"
@ -15,7 +16,6 @@ import (
// NewRouter builds and returns a *gin.Engine with all routes configured.
func (s *Server) NewRouter() *gin.Engine {
router := gin.Default()
router.Use(s.initAccountInfo())
// Pagination middleware setup
paginator := pagination.New(
@ -30,35 +30,34 @@ func (s *Server) NewRouter() *gin.Engine {
// Swagger route
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Auth
authRouter := router.Group("/v1/auth")
// API routes
v1 := router.Group("/v1")
v1.Use(s.initAccountInfo())
v1.Use(s.ensureAuthenticated())
{
authRouter.POST("/displayCode", auth.DisplayCodeHandler(s.authService))
authRouter.GET("/token", auth.TokenHandler(s.authService))
}
// Auth
v1.POST("/auth/display_code", auth.DisplayCodeHandler(s.authService))
v1.GET("/auth/token", auth.TokenHandler(s.authService))
// Read-only group
readOnly := router.Group("/v1")
// readOnly.Use(a.AuthMiddleware())
// readOnly.Use(a.PermissionMiddleware("read-only"))
{
readOnly.GET("/spaces", paginator, space.GetSpacesHandler(s.spaceService))
readOnly.GET("/spaces/:space_id/members", paginator, space.GetMembersHandler(s.spaceService))
readOnly.GET("/spaces/:space_id/objects", paginator, object.GetObjectsHandler(s.objectService))
readOnly.GET("/spaces/:space_id/objects/:object_id", object.GetObjectHandler(s.objectService))
readOnly.GET("/spaces/:space_id/objectTypes", paginator, object.GetTypesHandler(s.objectService))
readOnly.GET("/spaces/:space_id/objectTypes/:typeId/templates", paginator, object.GetTemplatesHandler(s.objectService))
readOnly.GET("/search", paginator, search.SearchHandler(s.searchService))
}
// Export
v1.POST("/spaces/:space_id/objects/:object_id/export/:format", export.GetObjectExportHandler(s.exportService))
v1.GET("/spaces/:space_id/objects/export/:format", export.GetSpaceExportHandler(s.exportService))
// Read-write group
readWrite := router.Group("/v1")
// readWrite.Use(a.AuthMiddleware())
// readWrite.Use(a.PermissionMiddleware("read-write"))
{
readWrite.POST("/spaces", space.CreateSpaceHandler(s.spaceService))
readWrite.POST("/spaces/:space_id/objects", object.CreateObjectHandler(s.objectService))
readWrite.PUT("/spaces/:space_id/objects/:object_id", object.UpdateObjectHandler(s.objectService))
// Object
v1.GET("/spaces/:space_id/objects", paginator, object.GetObjectsHandler(s.objectService))
v1.GET("/spaces/:space_id/objects/:object_id", object.GetObjectHandler(s.objectService))
v1.GET("/spaces/:space_id/object_types", paginator, object.GetTypesHandler(s.objectService))
v1.GET("/spaces/:space_id/object_types/:typeId/templates", paginator, object.GetTemplatesHandler(s.objectService))
v1.POST("/spaces/:space_id/objects", object.CreateObjectHandler(s.objectService))
v1.PUT("/spaces/:space_id/objects/:object_id", object.UpdateObjectHandler(s.objectService))
// Search
v1.GET("/search", paginator, search.SearchHandler(s.searchService))
// Space
v1.GET("/spaces", paginator, space.GetSpacesHandler(s.spaceService))
v1.GET("/spaces/:space_id/members", paginator, space.GetMembersHandler(s.spaceService))
v1.POST("/spaces", space.CreateSpaceHandler(s.spaceService))
}
return router

View file

@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/cmd/api/auth"
"github.com/anyproto/anytype-heart/cmd/api/export"
"github.com/anyproto/anytype-heart/cmd/api/object"
"github.com/anyproto/anytype-heart/cmd/api/search"
"github.com/anyproto/anytype-heart/cmd/api/space"
@ -28,6 +29,7 @@ type Server struct {
mwInternal core.MiddlewareInternal
authService *auth.AuthService
exportService *export.ExportService
objectService *object.ObjectService
spaceService *space.SpaceService
searchService *search.SearchService
@ -38,6 +40,7 @@ func NewServer(mw service.ClientCommandsServer, mwInternal core.MiddlewareIntern
s := &Server{
mwInternal: mwInternal,
authService: auth.NewService(mw),
exportService: export.NewService(mw),
objectService: object.NewService(mw),
spaceService: space.NewService(mw),
}

View file

@ -23,6 +23,7 @@ var (
ErrFailedOpenWorkspace = errors.New("failed to open workspace")
ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon")
ErrFailedCreateSpace = errors.New("failed to create space")
ErrBadInput = errors.New("bad input")
ErrNoMembersFound = errors.New("no members found")
ErrFailedListMembers = errors.New("failed to retrieve list of members")
)