【源码分析】
Notion api doc
- 主要是api的doc的文档的信息
- README.md
- tutorial: https://blog.kowalczyk.info/article/c9df78cbeaae4e0cb2848c9964bcfc94/using-notion-api-go-client.html
- API docs: https://pkg.go.dev/github.com/kjk/notionapi
You can learn how [I reverse-engineered the Notion API](https://blog.kowalczyk.info/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html) in order to write this library.
# Real-life usage
I use this API to publish my [blog](https://blog.kowalczyk.info/) and series of [programming books](https://www.programming-books.io/) from content stored in Notion.
doPost-请求的对象的处理操作
- 支持3次重试的处理操作
func (c *Client) doPost(uri string, body []byte) ([]byte, error) {
if c.httpPostOverride != nil {
return c.httpPostOverride(uri, body)
}
return c.doPostInternal(uri, body)
}
func (c *Client) doPostInternal(uri string, body []byte) ([]byte, error) {
c.rateLimitRequest()
nRepeats := 0
timeouts := []time.Duration{time.Second * 3, time.Second * 5, time.Second * 10}
repeatRequest:
br := bytes.NewBuffer(body)
req, err := http.NewRequest("POST", uri, br)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept-Language", acceptLang)
if c.AuthToken != "" {
req.Header.Set("cookie", fmt.Sprintf("token_v2=%v", c.AuthToken))
}
var rsp *http.Response
httpClient := c.getHTTPClient()
rsp, err = httpClient.Do(req)
if err != nil {
c.logf("httpClient.Do() failed with %s\n", err)
return nil, err
}
if rsp.StatusCode == http.StatusTooManyRequests {
if nRepeats < 3 {
closeNoError(rsp.Body)
c.logf("retrying '%s' because httpClient.Do() returned %d (%s)\n", uri, rsp.StatusCode, rsp.Status)
time.Sleep(timeouts[nRepeats])
nRepeats++
goto repeatRequest
}
}
defer closeNoError(rsp.Body)
if rsp.StatusCode != 200 {
d, _ := ioutil.ReadAll(rsp.Body)
c.logf("Error: status code %s\nBody:\n%s\n", rsp.Status, PrettyPrintJS(d))
return nil, fmt.Errorf("http.Post('%s') returned non-200 status code of %d", uri, rsp.StatusCode)
}
d, err := ioutil.ReadAll(rsp.Body)
if err != nil {
c.logf("Error: ioutil.ReadAll() failed with %s\n", err)
return nil, err
}
return d, nil
}
func (c *Client) doNotionAPI(apiURL string, requestData interface{}, result interface{}) (map[string]interface{}, error) {
var body []byte
var err error
if requestData != nil {
body, err = jsonit.MarshalIndent(requestData, "", " ")
if err != nil {
return nil, err
}
}
uri := notionHost + apiURL
c.logf("POST %s\n", uri)
if len(body) > 0 {
logJSON(c, body)
}
d, err := c.doPost(uri, body)
if err != nil {
return nil, err
}
logJSON(c, d)
err = jsonit.Unmarshal(d, result)
if err != nil {
c.logf("Error: json.Unmarshal() failed with %s\n. Body:\n%s\n", err, string(d))
return nil, err
}
var m map[string]interface{}
err = jsonit.Unmarshal(d, &m)
if err != nil {
return nil, err
}
return m, nil
}
Block-0.1-数据结构
- 这个是核心的结构,主要是为了构建这个block的结构,收费也是根据这个来的。
- block.go
type Block struct {
ID string `json:"id"`
Alive bool `json:"alive"`
ContentIDs []string `json:"content,omitempty"`
CopiedFrom string `json:"copied_from,omitempty"`
CollectionID string `json:"collection_id,omitempty"`
CreatedBy string `json:"created_by"`
CreatedTime int64 `json:"created_time"`
CreatedByTable string `json:"created_by_table"`
CreatedByID string `json:"created_by_id"`
LastEditedBy string `json:"last_edited_by"`
LastEditedTime int64 `json:"last_edited_time"`
LastEditedByTable string `json:"last_edited_by_table"`
LastEditedByID string `json:"last_edited_by_id"`
DiscussionIDs []string `json:"discussion,omitempty"`
FileIDs []string `json:"file_ids,omitempty"`
IgnoreBlockCount bool `json:"ignore_block_count,omitempty"`
ParentID string `json:"parent_id"`
ParentTable string `json:"parent_table"`
Permissions *[]Permission `json:"permissions,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"`
SpaceID string `json:"space_id"`
Type string `json:"type"`
Version int64 `json:"version"`
ViewIDs []string `json:"view_ids,omitempty"`
Parent *Block `json:"-"`
Content []*Block `json:"-"`
InlineContent []*TextSpan `json:"-"`
Title string `json:"-"`
IsChecked bool `json:"-"`
Description string `json:"-"`
Link string `json:"-"`
Source string `json:"-"`
FileSize string `json:"-"`
Code string `json:"-"`
CodeLanguage string `json:"-"`
TableViews []*TableView `json:"-"`
Page *Page `json:"-"`
RawJSON map[string]interface{} `json:"-"`
notionID *NotionID
parentNotionID *NotionID
isResolved bool
}
block-0.2-解析标题和属性的信息
func parseTitle(block *Block) error {
if block.InlineContent != nil {
return nil
}
props := block.Properties
title, ok := props["title"]
if !ok {
return nil
}
var err error
block.InlineContent, err = ParseTextSpans(title)
if err != nil {
return err
}
switch block.Type {
case BlockPage, BlockFile, BlockBookmark:
block.Title, err = getInlineText(title)
case BlockCode:
block.Code, err = getInlineText(title)
default:
}
return err
}
func parseProperties(block *Block) error {
err := parseTitle(block)
if err != nil {
return err
}
props := block.Properties
if BlockTodo == block.Type {
if checked, ok := props["checked"]; ok {
s, _ := getFirstInlineBlock(checked)
block.IsChecked = strings.EqualFold(s, "Yes")
}
}
getProp(block, "description", &block.Description)
getProp(block, "link", &block.Link)
if block.Source == "" {
getProp(block, "source", &block.Source)
}
getProp(block, "language", &block.CodeLanguage)
if block.Type == BlockFile {
getProp(block, "size", &block.FileSize)
}
return nil
}
func parseTextSpanAttributes(b *TextSpan, a []interface{}) error {
for _, rawAttr := range a {
attrList, ok := rawAttr.([]interface{})
if !ok {
return fmt.Errorf("rawAttr is not []interface{} but %T of value %#v", rawAttr, rawAttr)
}
err := parseTextSpanAttribute(b, attrList)
if err != nil {
return err
}
}
return nil
}
func parseTextSpan(a []interface{}) (*TextSpan, error) {
if len(a) == 0 {
return nil, fmt.Errorf("a is empty")
}
if len(a) == 1 {
s, ok := a[0].(string)
if !ok {
return nil, fmt.Errorf("a is of length 1 but not string. a[0] el type: %T, el value: '%#v'", a[0], a[0])
}
return &TextSpan{
Text: s,
}, nil
}
if len(a) != 2 {
return nil, fmt.Errorf("a is of length != 2. a value: '%#v'", a)
}
s, ok := a[0].(string)
if !ok {
return nil, fmt.Errorf("a[0] is not string. a[0] type: %T, value: '%#v'", a[0], a[0])
}
res := &TextSpan{
Text: s,
}
a, ok = a[1].([]interface{})
if !ok {
return nil, fmt.Errorf("a[1] is not []interface{}. a[1] type: %T, value: '%#v'", a[1], a[1])
}
err := parseTextSpanAttributes(res, a)
if err != nil {
return nil, err
}
return res, nil
}
func ParseTextSpans(raw interface{}) ([]*TextSpan, error) {
if raw == nil {
return nil, nil
}
var res []*TextSpan
a, ok := raw.([]interface{})
if !ok {
return nil, fmt.Errorf("raw is not of []interface{}. raw type: %T, value: '%#v'", raw, raw)
}
if len(a) == 0 {
return nil, fmt.Errorf("raw is empty")
}
for _, v := range a {
a2, ok := v.([]interface{})
if !ok {
return nil, fmt.Errorf("v is not []interface{}. v type: %T, value: '%#v'", v, v)
}
span, err := parseTextSpan(a2)
if err != nil {
return nil, err
}
res = append(res, span)
}
return res, nil
}
Json的工具的格式的处理
func PrettyPrintJSJsonit(js []byte) []byte {
if !jsonit.Valid(js) {
return js
}
return pretty.PrettyOptions(js, &prettyOpts)
}
func PrettyPrintJSStd(js []byte) []byte {
var m map[string]interface{}
err := json.Unmarshal(js, &m)
if err != nil {
return js
}
d, err := json.MarshalIndent(m, "", " ")
if err != nil {
return js
}
return d
}
func jsonUnmarshalFromMap(m map[string]interface{}, v interface{}) error {
d, err := jsonit.Marshal(m)
if err != nil {
return err
}
return json.Unmarshal(d, v)
}
func jsonGetMap(m map[string]interface{}, key string) map[string]interface{} {
if v, ok := m[key]; ok {
if m, ok := v.(map[string]interface{}); ok {
return m
}
}
return nil
}
Permission-权限的管理的处理
type Permission struct {
Type string `json:"type"`
Role interface{} `json:"role"`
UserID *string `json:"user_id,omitempty"`
AddedTimestamp int64 `json:"added_timestamp"`
AllowDuplicate bool `json:"allow_duplicate"`
AllowSearchEngineIndexing bool `json:"allow_search_engine_indexing"`
}
tableview-0.1-表格的支持多种视图的切换
type TableView struct {
Page *Page
CollectionView *CollectionView
Collection *Collection
Columns []*ColumnInfo
Rows []*TableRow
}
page-表格和page是多对多的关系,这样里面是可以相互嵌套的
type Page struct {
ID string
NotionID *NotionID
BlockRecords []*Record
UserRecords []*Record
CollectionRecords []*Record
CollectionViewRecords []*Record
DiscussionRecords []*Record
CommentRecords []*Record
SpaceRecords []*Record
TableViews []*TableView
idToBlock map[string]*Block
idToUser map[string]*User
idToCollection map[string]*Collection
idToCollectionView map[string]*CollectionView
idToComment map[string]*Comment
idToDiscussion map[string]*Discussion
idToSpace map[string]*Space
blocksToSkip map[string]struct{}
client *Client
subPages []*NotionID
}
Activity-类似于事件的日志,也就是每一个操作就会记录在activity里面下
- activity可以为每个操作记录下信息,尤其是对于block记录,方便后面进行收费的处理操作
- export_page.go
type Edit struct {
SpaceID string `json:"space_id"`
Authors []Author `json:"authors"`
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Version int `json:"version"`
CommentData Comment `json:"comment_data"`
CommentID string `json:"comment_id"`
DiscussionID string `json:"discussion_id"`
BlockID string `json:"block_id"`
BlockData struct {
BlockValue Block `json:"block_value"`
} `json:"block_data"`
NavigableBlockID string `json:"navigable_block_id"`
CollectionID string `json:"collection_id"`
CollectionRowID string `json:"collection_row_id"`
}
type Activity struct {
Role string `json:"role"`
ID string `json:"id"`
SpaceID string `json:"space_id"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Type string `json:"type"`
Version int `json:"version"`
ParentID string `json:"parent_id"`
ParentTable string `json:"parent_table"`
NavigableBlockID string `json:"navigable_block_id"`
CollectionID string `json:"collection_id"`
CollectionRowID string `json:"collection_row_id"`
Edits []Edit `json:"edits"`
Index int `json:"index"`
Invalid bool `json:"invalid"`
RawJSON map[string]interface{} `json:"-"`
}
func (c *Client) GetActivityLog(spaceID string, startingAfterID string, limit int) (*GetActivityLogResponse, error) {
req := &getActivityLogRequest{
SpaceID: spaceID,
StartingAfterID: startingAfterID,
Limit: limit,
}
var rsp GetActivityLogResponse
var err error
apiURL := "/api/v3/getActivityLog"
if rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp); err != nil {
return nil, err
}
if err = ParseRecordMap(rsp.RecordMap); err != nil {
return nil, err
}
if len(rsp.ActivityIDs) > 0 {
rsp.NextID = rsp.ActivityIDs[len(rsp.ActivityIDs)-1]
} else {
rsp.NextID = ""
}
return &rsp, nil
}
CollectionView-集合视图的情况
- 填充表格的,表格记录,包含了拥有的表格视图,页面快和列的文本块信息
type CollectionView struct {
ID string `json:"id"`
Version int64 `json:"version"`
Type string `json:"type"`
Format *FormatTable `json:"format"`
Name string `json:"name"`
ParentID string `json:"parent_id"`
ParentTable string `json:"parent_table"`
Query *Query `json:"query2"`
Alive bool `json:"alive"`
PageSort []string `json:"page_sort"`
SpaceID string `json:"space_id"`
RawJSON map[string]interface{} `json:"-"`
}
type FormatTable struct {
PageSort []string `json:"page_sort"`
TableWrap bool `json:"table_wrap"`
TableProperties []*TableProperty `json:"table_properties"`
}
type TableRow struct {
TableView *TableView
Page *Block
Columns [][]*TextSpan
}
type TextAttr = []string
type TextSpan struct {
Text string `json:"Text"`
Attrs []TextAttr `json:"Attrs"`
}
type Collection struct {
ID string `json:"id"`
Version int `json:"version"`
Name interface{} `json:"name"`
Schema map[string]*ColumnSchema `json:"schema"`
Format *CollectionFormat `json:"format"`
ParentID string `json:"parent_id"`
ParentTable string `json:"parent_table"`
Alive bool `json:"alive"`
CopiedFrom string `json:"copied_from"`
Cover string `json:"cover"`
Description []interface{} `json:"description"`
Type string `json:"type"`
FileIDs []string `json:"file_ids"`
Icon string `json:"icon"`
TemplatePages []string `json:"template_pages"`
name []*TextSpan
RawJSON map[string]interface{} `json:"-"`
}
space-用于描述workspace的情况,包括创建和构建的情况
- 把space的填充,把space的体系的构建起来的操作
- space.go
type Space struct {
ID string `json:"id"`
Version float64 `json:"version"`
Name string `json:"name"`
Domain string `json:"domain"`
Permissions []*SpacePermissions `json:"permissions,omitempty"`
PermissionGroups []SpacePermissionGroups `json:"permission_groups"`
Icon string `json:"icon"`
EmailDomains []string `json:"email_domains"`
BetaEnabled bool `json:"beta_enabled"`
Pages []string `json:"pages,omitempty"`
DisablePublicAccess bool `json:"disable_public_access"`
DisableGuests bool `json:"disable_guests"`
DisableMoveToSpace bool `json:"disable_move_to_space"`
DisableExport bool `json:"disable_export"`
CreatedBy string `json:"created_by"`
CreatedTime int64 `json:"created_time"`
LastEditedBy string `json:"last_edited_by"`
LastEditedTime int64 `json:"last_edited_time"`
RawJSON map[string]interface{} `json:"-"`
}
Query-查询过滤器, 对于查询的能力的吹了
- 把查询的结果信息并且存储起来,便于使用和操作
- collection.go
type QuerySort struct {
ID string `json:"id"`
Type string `json:"type"`
Property string `json:"property"`
Direction string `json:"direction"`
}
type QueryAggregate struct {
ID string `json:"id"`
Type string `json:"type"`
Property string `json:"property"`
ViewType string `json:"view_type"`
AggregationType string `json:"aggregation_type"`
}
type QueryAggregation struct {
Property string `json:"property"`
Aggregator string `json:"aggregator"`
}
type Query struct {
Sort []QuerySort `json:"sort"`
Aggregate []QueryAggregate `json:"aggregate"`
Aggregations []QueryAggregation `json:"aggregations"`
Filter map[string]interface{} `json:"filter"`
}
block-0.3-对于块的信息的处理和操作
- 对于block的数据库的处理操作,并且把数据集中处理下
- page.go
func forEachBlockWithParent(seen map[string]bool, blocks []*Block, parent *Block, cb func(*Block)) {
for _, block := range blocks {
id := block.ID
if seen[id] {
panic("seen the same page again")
}
if parent != nil && (block.Type == BlockPage || block.Type == BlockCollectionViewPage) {
continue
}
seen[id] = true
block.Parent = parent
cb(block)
forEachBlockWithParent(seen, block.Content, block, cb)
}
}
func ForEachBlock(blocks []*Block, cb func(*Block)) {
seen := map[string]bool{}
forEachBlockWithParent(seen, blocks, nil, cb)
}
func (p *Page) ForEachBlock(cb func(*Block)) {
seen := map[string]bool{}
blocks := []*Block{p.Root()}
forEachBlockWithParent(seen, blocks, nil, cb)
}
page-GetSubPages-获取子页面的数据信息
- 对于子页面的信息,并且遍历其特性和能力点
- page.go
func (p *Page) GetSubPages() []*NotionID {
if len(p.subPages) > 0 {
return p.subPages
}
root := p.Root()
panicIf(!isPageBlock(root))
subPages := map[*NotionID]struct{}{}
seenBlocks := map[string]struct{}{}
var blocksToVisit []*NotionID
for _, id := range root.ContentIDs {
nid := NewNotionID(id)
blocksToVisit = append(blocksToVisit, nid)
}
for len(blocksToVisit) > 0 {
nid := blocksToVisit[0]
id := nid.DashID
blocksToVisit = blocksToVisit[1:]
if _, ok := seenBlocks[id]; ok {
continue
}
seenBlocks[id] = struct{}{}
block := p.BlockByID(nid)
if p.IsSubPage(block) {
subPages[nid] = struct{}{}
}
for _, id := range block.ContentIDs {
nid := NewNotionID(id)
blocksToVisit = append(blocksToVisit, nid)
}
}
res := []*NotionID{}
for id := range subPages {
res = append(res, id)
}
sort.Slice(res, func(i, j int) bool {
return res[i].DashID < res[j].DashID
})
p.subPages = res
return res
}
export-把页面导出成html和markdown的格式用于备份的处理操作
func (c *Client) ExportPages(id string, exportType string, recursive bool) ([]byte, error) {
exportURL, err := c.RequestPageExportURL(id, exportType, recursive)
if err != nil {
return nil, err
}
dlRsp, err := c.DownloadFile(exportURL, nil)
if err != nil {
return nil, err
}
return dlRsp.Data, nil
}
func (c *Client) DownloadFile(uri string, block *Block) (*DownloadFileResponse, error) {
uri2 := maybeProxyImageURL(uri, block)
res, err := c.DownloadURL(uri2)
if err != nil && uri2 != uri {
res, err = c.DownloadURL(uri)
}
if err != nil {
rsp, err2 := c.GetSignedURLs([]string{uri}, block)
if err2 != nil {
return nil, err
}
if len(rsp.SignedURLS) == 0 {
return nil, err
}
uri3 := rsp.SignedURLS[0]
res, err = c.DownloadURL(uri3)
}
return res, err
}
record-parseRecord-根据对象获取解析的对象信息
- 对象信息的解析和能力的构建
- get_record_values.go
func parseRecord(table string, r *Record) error {
if len(r.Value) == 0 {
return nil
}
if r.Table == "" {
r.Table = table
} else {
panicIf(r.Table != table)
}
var pRawJSON *map[string]interface{}
var obj interface{}
switch table {
case TableActivity:
r.Activity = &Activity{}
obj = r.Activity
pRawJSON = &r.Activity.RawJSON
case TableBlock:
r.Block = &Block{}
obj = r.Block
pRawJSON = &r.Block.RawJSON
case TableUser:
r.User = &User{}
obj = r.User
pRawJSON = &r.User.RawJSON
case TableSpace:
r.Space = &Space{}
obj = r.Space
pRawJSON = &r.Space.RawJSON
case TableCollection:
r.Collection = &Collection{}
obj = r.Collection
pRawJSON = &r.Collection.RawJSON
case TableCollectionView:
r.CollectionView = &CollectionView{}
obj = r.CollectionView
pRawJSON = &r.CollectionView.RawJSON
case TableDiscussion:
r.Discussion = &Discussion{}
obj = r.Discussion
pRawJSON = &r.Discussion.RawJSON
case TableComment:
r.Comment = &Comment{}
obj = r.Comment
pRawJSON = &r.Comment.RawJSON
}
if obj == nil {
return fmt.Errorf("unsupported table '%s'", r.Table)
}
if err := jsonit.Unmarshal(r.Value, pRawJSON); err != nil {
return err
}
id := (*pRawJSON)["id"]
if id != nil {
r.ID = id.(string)
}
if err := jsonit.Unmarshal(r.Value, &obj); err != nil {
return err
}
return nil
}
SubscriptionData-订阅和付费的信息的处理
- 把订阅的服务和信息填充起来,便于统计需要付费的情况。
- get_subscription_data.go
type SubscriptionData struct {
Type string `json:"type"`
SpaceUsers []SubscriptionDataSpaceUsers `json:"spaceUsers"`
Credits []SubscriptionDataCredits `json:"credits"`
TotalCredit int `json:"totalCredit"`
AvailableCredit int `json:"availableCredit"`
CreditEnabled bool `json:"creditEnabled"`
CustomerID string `json:"customerId"`
CustomerName string `json:"customerName"`
VatID string `json:"vatId"`
IsDelinquent bool `json:"isDelinquent"`
ProductID string `json:"productId"`
BillingEmail string `json:"billingEmail"`
Plan string `json:"plan"`
PlanAmount int `json:"planAmount"`
AccountBalance int `json:"accountBalance"`
MonthlyPlanAmount int `json:"monthlyPlanAmount"`
YearlyPlanAmount int `json:"yearlyPlanAmount"`
Quantity int `json:"quantity"`
Billing string `json:"billing"`
Address SubscriptionDataAddress `json:"address"`
Last4 string `json:"last4"`
Brand string `json:"brand"`
Interval string `json:"interval"`
Created int64 `json:"created"`
PeriodEnd int64 `json:"periodEnd"`
NextInvoiceTime int64 `json:"nextInvoiceTime"`
NextInvoiceAmount int `json:"nextInvoiceAmount"`
IsPaid bool `json:"isPaid"`
Members []interface{} `json:"members"`
RawJSON map[string]interface{} `json:"-"`
}
func (c *Client) GetSubscriptionData(spaceID string) (*SubscriptionData, error) {
req := &struct {
SpaceID string `json:"spaceId"`
}{
SpaceID: spaceID,
}
var rsp SubscriptionData
var err error
apiURL := "/api/v3/getSubscriptionData"
rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp)
if err != nil {
return nil, err
}
return &rsp, nil
}
main-启动http的服务的能力
- 构建http的服务,然后把服务的能力的构建起来操作
- main.go
func startHTTPServer(uri string) {
flgHTTPAddr := "localhost:8503"
httpSrv := makeHTTPServer()
httpSrv.Addr = flgHTTPAddr
logf("Starting on addr: %v\n", flgHTTPAddr)
chServerClosed := make(chan bool, 1)
go func() {
err := httpSrv.ListenAndServe()
if err == http.ErrServerClosed {
err = nil
}
must(err)
logf("HTTP server shutdown gracefully\n")
chServerClosed <- true
}()
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt , syscall.SIGTERM)
openBrowser("http://" + flgHTTPAddr + uri)
time.Sleep(time.Second * 2)
sig := <-c
logf("Got signal %s\n", sig)
if httpSrv != nil {
_ = httpSrv.Shutdown(context.Background())
select {
case <-chServerClosed:
case <-time.After(time.Second * 5):
}
}
}
sync-对于指定的block进行同步和能力的构建操作
- 构建的体系和negligib的操作,并且支撑其能力的
- sync_record_values.go
func (c *Client) SyncRecordValues(records []SyncRecordRequest) (*SyncRecordValuesResponse, error) {
req := &syncRecordValuesRequest{
Requests: records,
}
var rsp SyncRecordValuesResponse
var err error
apiURL := "/api/v3/syncRecordValues"
if rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp); err != nil {
return nil, err
}
for table, records := range rsp.RecordMap {
for _, r := range records {
err = parseRecord(table, r)
rsp.Results = append(rsp.Results, r)
if err != nil {
return nil, err
}
}
}
return &rsp, nil
}
func (c *Client) SyncBlockRecords(ids []string) (*SyncRecordValuesResponse, error) {
records := make([]SyncRecordRequest, len(ids))
for pos, id := range ids {
dashID := ToDashID(id)
if !IsValidDashID(dashID) {
return nil, fmt.Errorf("'%s' is not a valid notion id", id)
}
r := &records[pos]
r.Version = -1
r.Pointer.Table = TableBlock
r.Pointer.ID = dashID
}
return c.SyncRecordValues(records)
}