Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/api/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type ClientCommands interface {
// Block
BlockCreate(context.Context, *pb.RpcBlockCreateRequest) *pb.RpcBlockCreateResponse
BlockPaste(context.Context, *pb.RpcBlockPasteRequest) *pb.RpcBlockPasteResponse
BlockReplace(context.Context, *pb.RpcBlockReplaceRequest) *pb.RpcBlockReplaceResponse
BlockListDelete(context.Context, *pb.RpcBlockListDeleteRequest) *pb.RpcBlockListDeleteResponse

// Chat
Expand Down
49 changes: 49 additions & 0 deletions core/api/core/mock_apicore/mock_ClientCommands.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions core/api/handler/block_object_link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"

apimodel "github.com/anyproto/anytype-heart/core/api/model"
"github.com/anyproto/anytype-heart/core/api/service"
"github.com/anyproto/anytype-heart/core/api/util"
)

// SetBlockObjectLinkHandler sets the object link on a block (text→link or link target update) via the editor replace path.
//
// @Summary Set block object link
// @Description Sets or updates a UI-style object link on the given block: a text block becomes a link block (card layout); a link block gets a new target and card layout. Uses the same internal BlockReplace path as the editor (links/backlinks follow normal derivation). Re-posting the same target upgrades Text preview links to Card when needed.
// @Id set_block_object_link
// @Tags Objects
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-11-08)
// @Param space_id path string true "Space id"
// @Param object_id path string true "Source object id (page/note containing the block)"
// @Param block_id path string true "Block id to turn into / update as object link"
// @Param body body apimodel.SetBlockObjectLinkRequest true "Target object id"
// @Success 200 {object} apimodel.SetBlockObjectLinkResponse "Link set (or already matched target)"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Object or block not found"
// @Failure 410 {object} util.GoneError "Object deleted"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /v1/spaces/{space_id}/objects/{object_id}/blocks/{block_id}/link [post]
func SetBlockObjectLinkHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
blockId := c.Param("block_id")

var req apimodel.SetBlockObjectLinkRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.CodeToApiError(http.StatusBadRequest, err.Error()))
return
}

out, err := s.SetBlockObjectLink(c.Request.Context(), spaceId, objectId, blockId, req)
code := util.MapErrorCode(err,
util.ErrToCode(util.ErrBad, http.StatusBadRequest),
util.ErrToCode(service.ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(service.ErrObjectDeleted, http.StatusGone),
util.ErrToCode(service.ErrBlockNotFound, http.StatusNotFound),
util.ErrToCode(service.ErrRequiredBlock, http.StatusBadRequest),
util.ErrToCode(service.ErrUnsupportedBlockForLink, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedRetrieveObject, http.StatusInternalServerError),
util.ErrToCode(service.ErrBlockReplaceFailed, http.StatusInternalServerError),
)
if code != http.StatusOK {
c.JSON(code, util.CodeToApiError(code, err.Error()))
return
}
c.JSON(http.StatusOK, out)
}
}

// DeleteBlockObjectLinkHandler deletes a link block (optional ?target_object_id= must match).
//
// @Summary Delete block object link
// @Description Removes a link block from the object. When target_object_id is provided, the link must point to that object or the request fails.
// @Id delete_block_object_link
// @Tags Objects
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-11-08)
// @Param space_id path string true "Space id"
// @Param object_id path string true "Source object id"
// @Param block_id path string true "Link block id"
// @Param target_object_id query string false "If set, must equal the link target"
// @Success 204 "Deleted"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Object, block, or link mismatch"
// @Failure 410 {object} util.GoneError "Object deleted"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /v1/spaces/{space_id}/objects/{object_id}/blocks/{block_id}/link [delete]
func DeleteBlockObjectLinkHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
blockId := c.Param("block_id")
optionalTarget := c.Query("target_object_id")

err := s.DeleteBlockObjectLink(c.Request.Context(), spaceId, objectId, blockId, optionalTarget)
code := util.MapErrorCode(err,
util.ErrToCode(service.ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(service.ErrObjectDeleted, http.StatusGone),
util.ErrToCode(service.ErrBlockNotFound, http.StatusNotFound),
util.ErrToCode(service.ErrNotLinkBlock, http.StatusNotFound),
util.ErrToCode(service.ErrTargetMismatch, http.StatusNotFound),
util.ErrToCode(service.ErrRequiredBlock, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedRetrieveObject, http.StatusInternalServerError),
util.ErrToCode(service.ErrBlockDeleteFailed, http.StatusInternalServerError),
)
if code != http.StatusOK {
c.JSON(code, util.CodeToApiError(code, err.Error()))
return
}
c.Status(http.StatusNoContent)
}
}
16 changes: 10 additions & 6 deletions core/api/handler/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"net/http"
"strings"

"github.com/gin-gonic/gin"

Expand Down Expand Up @@ -66,11 +67,12 @@ func ListObjectsHandler(s *service.Service) gin.HandlerFunc {
// @Param space_id path string true "The ID of the space in which the object exists; must be retrieved from ListSpaces endpoint"
// @Param object_id path string true "The ID of the object to retrieve; must be retrieved from ListObjects, SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Param format query apimodel.BodyFormat false "The format to return the object body in" default(md)
// @Success 200 {object} apimodel.ObjectResponse "The retrieved object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Param block_link_candidates query string false "If `1` or `true`, includes block_link_candidates (block ids for POST …/blocks/{id}/link)" Enums(1,true)
// @Success 200 {object} apimodel.ObjectResponse "The retrieved object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /v1/spaces/{space_id}/objects/{object_id} [get]
func GetObjectHandler(s *service.Service) gin.HandlerFunc {
Expand All @@ -79,7 +81,9 @@ func GetObjectHandler(s *service.Service) gin.HandlerFunc {
objectId := c.Param("object_id")
// format := c.Query("format") // TODO: implement multiple formats

object, err := s.GetObject(c.Request.Context(), spaceId, objectId)
q := strings.TrimSpace(c.Query("block_link_candidates"))
withCandidates := q == "1" || strings.EqualFold(q, "true")
object, err := s.GetObject(c.Request.Context(), spaceId, objectId, withCandidates)
code := util.MapErrorCode(err,
util.ErrToCode(service.ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(service.ErrObjectDeleted, http.StatusGone),
Expand Down
30 changes: 30 additions & 0 deletions core/api/model/block_object_link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package apimodel

// SetBlockObjectLinkRequest sets or updates an object link on a block (UI-equivalent: link block / text→link).
type SetBlockObjectLinkRequest struct {
TargetObjectId string `json:"target_object_id" binding:"required" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"`
// LinkStyle embed layout: page (default for new from text), dataview, dashboard, archive. Omit to keep existing on link blocks or use page for new text→link.
LinkStyle string `json:"link_style,omitempty" example:"dashboard"`
// CardStyle presentation: text, card (default for new from text), inline. Omit to keep existing on link blocks or use card for new text→link.
CardStyle string `json:"card_style,omitempty" example:"card"`
// SyncLinkPresentationFromBlockId, if set, must be the id of another link block on the same page. IconSize, Description, Relations, Fields and (unless link_style / card_style are set) Style and CardStyle are copied from that block so the result matches a manually tuned card (e.g. description snippet + type). target_object_id is always taken from this request.
SyncLinkPresentationFromBlockId string `json:"sync_link_presentation_from_block_id,omitempty" example:"69e29106f6ec12739aaf32e6"`
// BackgroundColor sets the block highlight (palette keys only: grey, yellow, orange, red, pink, purple, blue, ice, teal, lime — same as tags; UI «Зелёный» = lime, not "green"). Pointer: null/absent = unchanged, "" = clear.
BackgroundColor *string `json:"background_color,omitempty" extensions:"nullable" example:"lime"`
// IconSize: none, small, medium. Omit to keep existing.
IconSize string `json:"icon_size,omitempty" example:"medium"`
// LinkDescription: none, added, content. Omit to keep existing.
LinkDescription string `json:"link_description,omitempty" example:"content"`
// Relations lists relation keys to show on the card (e.g. "github_stars", "tag"). Omit to keep existing.
Relations []string `json:"relations,omitempty" example:"[\"github_stars\",\"tag\"]"`
}

// SetBlockObjectLinkResponse returns the block id after replace (may differ from the request path id).
type SetBlockObjectLinkResponse struct {
Object string `json:"object" example:"block_object_link"`
BlockId string `json:"block_id"`
ObjectId string `json:"object_id"`
SpaceId string `json:"space_id"`
TargetId string `json:"target_object_id"`
Replaced bool `json:"replaced"` // false when target was already set (idempotent)
}
24 changes: 23 additions & 1 deletion core/api/model/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type UpdateObjectRequest struct {
TypeKey *string `json:"type_key" example:"page"` // The key of the type of object to set
Properties *[]PropertyLinkWithValue `json:"properties" oneOf:"TextPropertyLinkValue,NumberPropertyLinkValue,SelectPropertyLinkValue,MultiSelectPropertyLinkValue,DatePropertyLinkValue,FilesPropertyLinkValue,CheckboxPropertyLinkValue,UrlPropertyLinkValue,EmailPropertyLinkValue,PhonePropertyLinkValue,ObjectsPropertyLinkValue"` // The properties to set for the object; see ListTypes or GetType endpoints for linked properties
Markdown *string `json:"markdown" example:"This is the updated body of the object. Markdown syntax is supported here."` // The updated body of the object
// MarkdownAppend appends markdown as new blocks at the end without removing existing content (cannot be combined with markdown).
MarkdownAppend *string `json:"markdown_append,omitempty" example:"Paragraph added via API; use GET ?block_link_candidates=1 for block ids, then POST …/link."`
}

type ObjectResponse struct {
Expand Down Expand Up @@ -87,8 +89,28 @@ type ObjectWithBody struct {
Snippet string `json:"snippet" example:"The beginning of the object body..."` // The snippet of the object, especially important for notes as they don't have a name
Layout ObjectLayout `json:"layout" example:"basic"` // The layout of the object
Type *Type `json:"type" extensions:"nullable"` // The type of the object, or null if the type has been deleted.
Properties []PropertyWithValue `json:"properties" oneOf:"TextPropertyValue,NumberPropertyValue,SelectPropertyValue,MultiSelectPropertyValue,DatePropertyValue,FilesPropertyValue,CheckboxPropertyValue,UrlPropertyValue,EmailPropertyValue,PhonePropertyValue,ObjectsPropertyValue"` // The properties of the object
Properties []PropertyWithValue `json:"properties" oneOf:"TextPropertyValue,NumberPropertyValue,SelectPropertyValue,MultiPropertyValue,DatePropertyValue,FilesPropertyValue,CheckboxPropertyValue,UrlPropertyValue,EmailPropertyValue,PhonePropertyValue,ObjectsPropertyValue"` // The properties of the object
Markdown string `json:"markdown" example:"# This is the title\n..."` // The markdown body of the object
// BlockLinkCandidates is set only when GET object is called with ?block_link_candidates=1 (then non-nil, maybe empty).
BlockLinkCandidates *[]BlockLinkCandidate `json:"block_link_candidates,omitempty"`
}

// BlockLinkCandidate describes a block that can be passed to POST …/blocks/{block_id}/link (text or existing link).
type BlockLinkCandidate struct {
Object string `json:"object" example:"block_link_candidate"`
Id string `json:"id" example:"64394517de52ad5acb89c66c"`
Kind string `json:"kind" enums:"text,link" example:"text"` // "text" or "link"
TextPreview string `json:"text_preview,omitempty" example:"First words of the text block…"`
TargetObjectId string `json:"target_object_id,omitempty" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"`
// LinkStyle / CardStyle are set for kind "link" only (current block presentation; pass to POST …/link as link_style / card_style).
LinkStyle string `json:"link_style,omitempty" example:"page"`
CardStyle string `json:"card_style,omitempty" example:"card"`
// IconSize / LinkDescription / Relations reflect link block details (for rich cards; use sync_link_presentation_from_block_id to copy from another link).
IconSize string `json:"icon_size,omitempty" example:"medium"`
LinkDescription string `json:"link_description,omitempty" example:"content"`
Relations []string `json:"relations,omitempty"`
// BackgroundColor is the block highlight (grey, yellow, orange, red, pink, purple, blue, ice, teal, lime — not "green").
BackgroundColor string `json:"background_color,omitempty" example:"lime"`
}

// ! Deprecated schemas, until json blocks properly implemented
Expand Down
15 changes: 15 additions & 0 deletions core/api/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (srv *Server) NewRouter(mw apicore.ClientCommands, eventService apicore.Eve
srv.registerObjectRoutes(v1, eventService, writeRateLimitMW)
srv.registerPropertyRoutes(v1, eventService, writeRateLimitMW)
srv.registerSearchRoutes(v1, eventService)
srv.registerBlockRoutes(v1, eventService, writeRateLimitMW)
srv.registerSpaceRoutes(v1, eventService, writeRateLimitMW)
srv.registerTagRoutes(v1, eventService, writeRateLimitMW)
srv.registerTemplateRoutes(v1, eventService)
Expand Down Expand Up @@ -399,3 +400,17 @@ func (srv *Server) registerTypeRoutes(v1 *gin.RouterGroup, eventService apicore.
handler.DeleteTypeHandler(srv.service),
)
}

// registerBlockRoutes registers block-related routes
func (srv *Server) registerBlockRoutes(v1 *gin.RouterGroup, eventService apicore.EventService, writeRateLimitMW gin.HandlerFunc) {
v1.POST("/spaces/:space_id/objects/:object_id/blocks/:block_id/link",
writeRateLimitMW,
ensureAnalyticsEvent("SetBlockObjectLink", eventService),
handler.SetBlockObjectLinkHandler(srv.service),
)
v1.DELETE("/spaces/:space_id/objects/:object_id/blocks/:block_id/link",
writeRateLimitMW,
ensureAnalyticsEvent("DeleteBlockObjectLink", eventService),
handler.DeleteBlockObjectLinkHandler(srv.service),
)
}
Loading
Loading