From b2af8b7d0b78feeb1a759d09ad5f5649a42d5727 Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Sat, 15 May 2021 13:42:01 +0200 Subject: [PATCH] Add "create a page" endpoint --- README.md | 2 +- block.go | 57 +++++++++++++++++++++++++++++++++++ client.go | 40 +++++++++++++++++++++++++ database.go | 56 +++++++++++++++++++++++----------- page.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++---- rich_text.go | 85 +++++++++++++++++++++++++++++++++++++--------------- util.go | 23 ++++++++++++++ 7 files changed, 299 insertions(+), 47 deletions(-) create mode 100644 block.go create mode 100644 util.go diff --git a/README.md b/README.md index 9540397..45b73b3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Go client for the [Notion API](https://developers.notion.com/reference). ## Pages - [x] [Retrieve a page](client.go) -- [ ] Create a page +- [x] [Create a page](client.go) - [ ] Update page properties ### Blocks diff --git a/block.go b/block.go new file mode 100644 index 0000000..398f764 --- /dev/null +++ b/block.go @@ -0,0 +1,57 @@ +package notion + +import "time" + +// Block represents content on the Notion platform. +// See: https://developers.notion.com/reference/block +type Block struct { + Object string `json:"object"` + ID string `json:"id,omitempty"` + Type BlockType `json:"type"` + CreatedTime *time.Time `json:"created_time,omitempty"` + LastEditedTime *time.Time `json:"last_edited_time,omitempty"` + HasChildren bool `json:"has_children"` + + Paragraph *RichTextBlock `json:"paragraph,omitempty"` + Heading1 *Heading `json:"heading_1,omitempty"` + Heading2 *Heading `json:"heading_2,omitempty"` + Heading3 *Heading `json:"heading_3,omitempty"` + BulletedListItem *RichTextBlock `json:"bulleted_list_item,omitempty"` + NumberedListItem *RichText `json:"numbered_list_item,omitempty"` + ToDo *ToDo `json:"to_do,omitempty"` + Toggle *RichTextBlock `json:"toggle,omitempty"` + ChildPage *ChildPage `json:"rich_text,omitempty"` +} + +type RichTextBlock struct { + Text []RichText `json:"text"` + Children []Block `json:"children,omitempty"` +} + +type Heading struct { + Text []RichText `json:"text"` +} + +type ToDo struct { + RichTextBlock + Checked *bool `json:"checked,omitempty"` +} + +type ChildPage struct { + Title string `json:"title"` +} + +type BlockType string + +const ( + BlockTypeParagraph BlockType = "paragraph" + BlockTypeHeading1 BlockType = "heading_1" + BlockTypeHeading2 BlockType = "heading_2" + BlockTypeHeading3 BlockType = "heading_3" + BlockTypeBulletedListItem BlockType = "bulleted_list_item" + BlockTypeNumberedListItem BlockType = "numbered_list_item" + BlockTypeToDo BlockType = "to_do" + BlockTypeToggle BlockType = "toggle" + BlockTypeChildPage BlockType = "child_page" + BlockTypeUnsupported BlockType = "unsupported" +) diff --git a/client.go b/client.go index 86c0711..bb722b5 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" ) const ( @@ -146,3 +147,42 @@ func (c *Client) FindPageByID(ctx context.Context, id string) (page Page, err er return page, nil } + +// CreatePage creates a new page in the specified database or as a child of an existing page. +// See: https://developers.notion.com/reference/post-page +func (c *Client) CreatePage(ctx context.Context, params CreatePageParams) (page Page, err error) { + if err := params.Validate(); err != nil { + return Page{}, fmt.Errorf("notion: invalid page params: %w", err) + } + + body := &bytes.Buffer{} + + _ = json.NewEncoder(os.Stderr).Encode(params) + + err = json.NewEncoder(body).Encode(params) + if err != nil { + return Page{}, fmt.Errorf("notion: failed to encode body params to JSON: %w", err) + } + + req, err := c.newRequest(ctx, http.MethodPost, "/pages", body) + if err != nil { + return Page{}, fmt.Errorf("notion: invalid request: %w", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return Page{}, fmt.Errorf("notion: failed to make HTTP request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return Page{}, fmt.Errorf("notion: failed to create page: %w", parseErrorResponse(res)) + } + + err = json.NewDecoder(res.Body).Decode(&page) + if err != nil { + return Page{}, fmt.Errorf("notion: failed to parse HTTP response: %w", err) + } + + return page, nil +} diff --git a/database.go b/database.go index efa4149..ce01264 100644 --- a/database.go +++ b/database.go @@ -49,8 +49,8 @@ type SelectOptions struct { } type DatabaseProperty struct { - ID string `json:"id"` - Type string `json:"type"` + ID string `json:"id"` + Type DatabasePropertyType `json:"type"` Number *NumberMetadata `json:"number"` Select *SelectMetadata `json:"select"` @@ -136,19 +136,19 @@ type MultiSelectDatabaseQueryFilter struct { } type DateDatabaseQueryFilter struct { - Equals time.Time `json:"equals,omitempty"` - Before time.Time `json:"before,omitempty"` - After time.Time `json:"after,omitempty"` - OnOrBefore time.Time `json:"on_or_before,omitempty"` - OnOrAfter time.Time `json:"on_or_after,omitempty"` - IsEmpty bool `json:"is_empty,omitempty"` - IsNotEmpty bool `json:"is_not_empty,omitempty"` - PastWeek *struct{} `json:"past_week,omitempty"` - PastMonth *struct{} `json:"past_month,omitempty"` - PastYear *struct{} `json:"past_year,omitempty"` - NextWeek *struct{} `json:"next_week,omitempty"` - NextMonth *struct{} `json:"next_month,omitempty"` - NextYear *struct{} `json:"next_year,omitempty"` + Equals *time.Time `json:"equals,omitempty"` + Before *time.Time `json:"before,omitempty"` + After *time.Time `json:"after,omitempty"` + OnOrBefore *time.Time `json:"on_or_before,omitempty"` + OnOrAfter *time.Time `json:"on_or_after,omitempty"` + IsEmpty bool `json:"is_empty,omitempty"` + IsNotEmpty bool `json:"is_not_empty,omitempty"` + PastWeek *struct{} `json:"past_week,omitempty"` + PastMonth *struct{} `json:"past_month,omitempty"` + PastYear *struct{} `json:"past_year,omitempty"` + NextWeek *struct{} `json:"next_week,omitempty"` + NextMonth *struct{} `json:"next_month,omitempty"` + NextYear *struct{} `json:"next_year,omitempty"` } type PeopleDatabaseQueryFilter struct { @@ -184,11 +184,33 @@ type DatabaseQuerySort struct { } type ( - SortTimestamp string - SortDirection string + DatabasePropertyType string + SortTimestamp string + SortDirection string ) const ( + // Database property type enums. + DBPropTypeTitle DatabasePropertyType = "title" + DBPropTypeRichText DatabasePropertyType = "rich_text" + DBPropTypeNumber DatabasePropertyType = "number" + DBPropTypeSelect DatabasePropertyType = "select" + DBPropTypeMultiSelect DatabasePropertyType = "multi_select" + DBPropTypeDate DatabasePropertyType = "date" + DBPropTypePeople DatabasePropertyType = "people" + DBPropTypeFile DatabasePropertyType = "file" + DBPropTypeCheckbox DatabasePropertyType = "checkbox" + DBPropTypeURL DatabasePropertyType = "url" + DBPropTypeEmail DatabasePropertyType = "email" + DBPropTypePhoneNumber DatabasePropertyType = "phone_number" + DBPropTypeFormula DatabasePropertyType = "formula" + DBPropTypeRelation DatabasePropertyType = "relation" + DBPropTypeRollup DatabasePropertyType = "rollup" + DBPropTypeCreatedTime DatabasePropertyType = "created_time" + DBPropTypeCreatedBy DatabasePropertyType = "created_by" + DBPropTypeLastEditedTime DatabasePropertyType = "last_edited_time" + DBPropTypeLastEditedBy DatabasePropertyType = "last_edited_by" + // Sort timestamp enums. SortTimeStampCreatedTime SortTimestamp = "created_time" SortTimeStampLastEditedTime SortTimestamp = "last_edited_time" diff --git a/page.go b/page.go index 55800b6..ffa05af 100644 --- a/page.go +++ b/page.go @@ -2,6 +2,7 @@ package notion import ( "encoding/json" + "errors" "fmt" "time" ) @@ -24,15 +25,17 @@ type Page struct { type PageParent struct { Type string `json:"type"` - PageID *string `json:"page_id"` - DatabaseID *string `json:"database_id"` + PageID *string `json:"page_id,omitempty"` + DatabaseID *string `json:"database_id,omitempty"` } // PageProperties are properties of a page whose parent is a page or a workspace. type PageProperties struct { - Title struct { - Title []RichText `json:"title"` - } `json:"title"` + Title PageTitle `json:"title"` +} + +type PageTitle struct { + Title []RichText `json:"title"` } // DatabasePageProperties are properties of a page whose parent is a database. @@ -45,6 +48,76 @@ type DatabasePageProperty struct { MultiSelect []SelectOptions `json:"multi_select"` } +// CreatePageParams are the params used for creating a page. +type CreatePageParams struct { + ParentType ParentType + ParentID string + + // Either DatabasePageProperties or Title must be not nil. + DatabasePageProperties *DatabasePageProperties + Title []RichText + + // Optionally, children blocks are added to the page. + Children []Block +} + +type ParentType string + +const ( + ParentTypeDatabase ParentType = "database_id" + ParentTypePage ParentType = "page_id" +) + +func (p CreatePageParams) Validate() error { + if p.ParentType == "" { + return errors.New("parent type is required") + } + if p.ParentID == "" { + return errors.New("parent ID is required") + } + if p.ParentType == ParentTypeDatabase && p.DatabasePageProperties == nil { + return errors.New("database page properties is required when parent type is database") + } + if p.ParentType == ParentTypePage && p.Title == nil { + return errors.New("title is required when parent type is page") + } + + return nil +} + +func (p CreatePageParams) MarshalJSON() ([]byte, error) { + type CreatePageParamsDTO struct { + Parent PageParent `json:"parent"` + Properties interface{} `json:"properties"` + Children []Block `json:"children,omitempty"` + } + + var parent PageParent + + if p.DatabasePageProperties != nil { + parent.Type = "database_id" + parent.DatabaseID = StringPtr(p.ParentID) + } else if p.Title != nil { + parent.Type = "page_id" + parent.PageID = StringPtr(p.ParentID) + } + + dto := CreatePageParamsDTO{ + Parent: parent, + Children: p.Children, + } + + if p.DatabasePageProperties != nil { + dto.Properties = p.DatabasePageProperties + } else if p.Title != nil { + dto.Properties = PageTitle{ + Title: p.Title, + } + } + + return json.Marshal(dto) +} + // UnmarshalJSON implements json.Unmarshaler. // // Pages get a different Properties type based on the parent of the page. diff --git a/rich_text.go b/rich_text.go index b6c44b8..b3d4566 100644 --- a/rich_text.go +++ b/rich_text.go @@ -3,14 +3,14 @@ package notion import "time" type RichText struct { - PlainText string `json:"plain_text"` - HRef *string `json:"href"` - Annotations Annotations `json:"annotations"` - Type string `json:"type"` + Type RichTextType `json:"type"` + Annotations *Annotations `json:"annotations,omitempty"` - Text *Text `json:"text"` - Mention *Mention `json:"mention"` - Equation *Equation `json:"equation"` + PlainText string `json:"plain_text,omitempty"` + HRef *string `json:"href,omitempty"` + Text *Text `json:"text,omitempty"` + Mention *Mention `json:"mention,omitempty"` + Equation *Equation `json:"equation,omitempty"` } type Equation struct { @@ -18,41 +18,78 @@ type Equation struct { } type Annotations struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Strikethrough bool `json:"strikethrough"` - Underline bool `json:"underline"` - Code bool `json:"code"` - Color string `json:"color"` + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Strikethrough bool `json:"strikethrough,omitempty"` + Underline bool `json:"underline,omitempty"` + Code bool `json:"code,omitempty"` + Color Color `json:"color,omitempty"` } type Mention struct { - Type string `json:"type"` + Type MentionType `json:"type"` - User *User `json:"user"` - Page *PageMention `json:"page"` - Database *DatabaseMention `json:"database"` - Date *Date `json:"date"` + User *User `json:"user,omitempty"` + Page *ID `json:"page,omitempty"` + Database *ID `json:"database,omitempty"` + Date *Date `json:"date,omitempty"` } type Date struct { Start time.Time `json:"start"` - End *time.Time `json:"end"` + End *time.Time `json:"end,omitempty"` } type Text struct { Content string `json:"content"` - Link *Link `json:"link"` + Link *Link `json:"link,omitempty"` } type Link struct { URL string `json:"url"` } -type PageMention struct { +type ID struct { ID string `json:"id"` } -type DatabaseMention struct { - ID string `json:"id"` -} +type ( + RichTextType string + MentionType string + Color string +) + +const ( + RichTextTypeText RichTextType = "text" + RichTextTypeMention RichTextType = "mention" + RichTextTypeEquation RichTextType = "equation" +) + +const ( + MentionTypeUser MentionType = "user" + MentionTypePage MentionType = "page" + MentionTypeDatabase MentionType = "database" + MentionTypeDate MentionType = "date" +) + +const ( + ColorDefault Color = "default" + ColorGray Color = "gray" + ColorBrown Color = "brown" + ColorOrange Color = "orange" + ColorYellow Color = "yellow" + ColorGreen Color = "green" + ColorBlue Color = "blue" + ColorPurple Color = "purple" + ColorPink Color = "pink" + ColorRed Color = "red" + ColorGrayBg Color = "gray_background" + ColorBrownBg Color = "brown_background" + ColorOrangeBg Color = "orange_background" + ColorYellowBg Color = "yellow_background" + ColorGreenBg Color = "green_background" + ColorBlueBg Color = "blue_background" + ColorPurpleBg Color = "purple_background" + ColorPinkBg Color = "pink_background" + ColorRedBg Color = "red_background" +) diff --git a/util.go b/util.go new file mode 100644 index 0000000..9d6ff98 --- /dev/null +++ b/util.go @@ -0,0 +1,23 @@ +package notion + +import "time" + +// StringPtr returns the pointer of a string value. +func StringPtr(s string) *string { + return &s +} + +// IntPtr returns the pointer of an int value. +func IntPtr(i int) *int { + return &i +} + +// TimePtr returns the pointer of a time.Time value. +func BoolPtr(b bool) *bool { + return &b +} + +// TimePtr returns the pointer of a time.Time value. +func TimePtr(t time.Time) *time.Time { + return &t +}