commit e9e9dca708e2e9dccc1690f726847216d39ee768 Author: David Stotijn Date: Thu May 13 22:11:32 2021 +0200 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a734352 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 David Stotijn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b88d8b --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# go-notion + +Go client for the [Notion API](https://developers.notion.com/reference). + +## Status + +🐣 Early development + +## API endpoints + +### Databases + +- [x] [Retrieve a database](database.go) +- [ ] Query a database +- [ ] List databases + +## Pages + +- [ ] Retrieve a page +- [ ] Create a page +- [ ] Update page properties + +### Blocks + +- [ ] Retrieve block children +- [ ] Append block children + +### Users + +- [ ] Retrieve a user +- [ ] List all users + +### Search + +- [ ] Search + +## License + +[MIT License](LICENSE) + +--- + +© 2021 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com) diff --git a/client.go b/client.go new file mode 100644 index 0000000..3b026f9 --- /dev/null +++ b/client.go @@ -0,0 +1,54 @@ +package notion + +import ( + "fmt" + "io" + "net/http" +) + +const ( + baseURL = "https://api.notion.com/v1" + apiVersion = "2021-05-13" +) + +// Client is used for HTTP requests to the Notion API. +type Client struct { + apiKey string + httpClient *http.Client +} + +// ClientOption is used to override default client behavior. +type ClientOption func(*Client) + +// NewClient returns a new Client. +func NewClient(apiKey string, opts ...ClientOption) *Client { + c := &Client{ + apiKey: apiKey, + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +// WithHTTPClient overrides the default http.Client. +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = httpClient + } +} + +func (c *Client) newRequest(method, url string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, baseURL+url, body) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.apiKey)) + req.Header.Set("Notion-Version", apiVersion) + + return req, nil +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..379d127 --- /dev/null +++ b/database.go @@ -0,0 +1,113 @@ +package notion + +import ( + "encoding/json" + "fmt" + "time" +) + +// Database is a resource on the Notion platform. +// See: https://developers.notion.com/reference/database +type Database struct { + ID string `json:"id"` + CreatedTime time.Time `json:"created_time"` + LastEditedTime time.Time `json:"last_edited_time"` + Title []RichText `json:"title"` + Properties DatabaseProperties `json:"properties"` +} + +type DatabaseProperties map[string]interface{} + +// Database property types. +type ( + TitleDatabaseProperty struct{} + RichTextDatabaseProperty struct{} + DateDatabaseProperty struct{} + PeopleDatabaseProperty struct{} + FileDatabaseProperty struct{} + CheckboxDatabaseProperty struct{} + URLDatabaseProperty struct{} + EmailDatabaseProperty struct{} + PhoneNumberDatabaseProperty struct{} + CreatedTimeDatabaseProperty struct{} + CreatedByDatabaseProperty struct{} + LastEditedTimeDatabaseProperty struct{} + LastEditedByDatabaseProperty struct{} + + NumberDatabaseProperty struct { + Format string `json:"format"` + } + SelectDatabaseProperty struct { + Options []SelectOptions `json:"options"` + } + FormulaDatabaseProperty struct { + Expression string `json:"expression"` + } + RelationDatabaseProperty struct { + DatabaseID string `json:"database_id"` + SyncedPropName *string `json:"synced_property_name"` + SyncedPropID *string `json:"synced_property_id"` + } + RollupDatabaseProperty struct { + RelationPropName string `json:"relation_property_name"` + RelationPropID string `json:"relation_property_id"` + RollupPropName string `json:"rollup_property_name"` + RollupPropID string `json:"rollup_property_id"` + Function string `json:"function"` + } +) + +type SelectOptions struct { + ID string `json:"id"` + Name string `json:"name"` + Color string `json:"color"` +} + +type DatabaseProperty struct { + ID string `json:"id"` + Type string `json:"type"` + + Title *TitleDatabaseProperty `json:"title"` + RichText *RichTextDatabaseProperty `json:"rich_text"` + Number *NumberDatabaseProperty `json:"number"` + Select *SelectDatabaseProperty `json:"select"` + MultiSelect *SelectDatabaseProperty `json:"multi_select"` + Date *DateDatabaseProperty `json:"date"` + People *PeopleDatabaseProperty `json:"people"` + File *FileDatabaseProperty `json:"file"` + Checkbox *CheckboxDatabaseProperty `json:"checkbox"` + URL *URLDatabaseProperty `json:"url"` + Email *EmailDatabaseProperty `json:"email"` + PhoneNumber *PhoneNumberDatabaseProperty `json:"phone_number"` + Formula *FormulaDatabaseProperty `json:"formula"` + Relation *RelationDatabaseProperty `json:"relation"` + Rollup *RollupDatabaseProperty `json:"rollup"` + CreatedTime *CreatedTimeDatabaseProperty `json:"created_time"` + CreatedBy *CreatedByDatabaseProperty `json:"created_by"` + LastEditedTime *LastEditedTimeDatabaseProperty `json:"last_edited_time"` + LastEditedBy *LastEditedByDatabaseProperty `json:"last_edited_by"` +} + +func (c *Client) FindDatabaseByID(id string) (db Database, err error) { + req, err := c.newRequest("GET", "/databases/"+id, nil) + if err != nil { + return Database{}, fmt.Errorf("invalid URL: %w", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return Database{}, fmt.Errorf("failed to make HTTP request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return Database{}, fmt.Errorf("notion: failed to get database: %w", parseErrorResponse(res)) + } + + err = json.NewDecoder(res.Body).Decode(&db) + if err != nil { + return Database{}, fmt.Errorf("failed to parse HTTP response: %w", err) + } + + return db, nil +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..12a9f2e --- /dev/null +++ b/error.go @@ -0,0 +1,75 @@ +package notion + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +// See: https://developers.notion.com/reference/errors. +var ( + ErrInvalidJSON = errors.New("notion: request body could not be decoded as JSON") + ErrInvalidRequestURL = errors.New("notion: request URL is not valid") + ErrInvalidRequest = errors.New("notion: request is not supported") + ErrValidation = errors.New("notion: request body does not match the schema for the expected parameters") + ErrUnauthorized = errors.New("notion: bearer token is not valid") + ErrRestrictedResource = errors.New("notion: client doesn't have permission to perform this operation") + ErrObjectNotFound = errors.New("notion: the resource does not exist") + ErrConflict = errors.New("notion: the transaction could not be completed, potentially due to a data collision") + ErrRateLimited = errors.New("notion: this request exceeds the number of requests allowed") + ErrInternalServer = errors.New("notion: an unexpected error occurred") + ErrServiceUnavailable = errors.New("notion: service is unavailable") +) + +type APIError struct { + Object string `json:"object"` + Status int `json:"status"` + Code string `json:"code"` + Message string `json:"message"` +} + +// Error implements `error`. +func (err *APIError) Error() string { + return fmt.Sprintf("%v (code: %v, status: %v)", err.Message, err.Code, err.Status) +} + +func (err *APIError) Unwrap() error { + switch err.Code { + case "invalid_json": + return ErrInvalidJSON + case "invalid_request_url": + return ErrInvalidRequestURL + case "invalid_request": + return ErrInvalidRequest + case "validation_error": + return ErrValidation + case "unauthorized": + return ErrUnauthorized + case "restricted_resource": + return ErrRestrictedResource + case "object_not_found": + return ErrObjectNotFound + case "conflict_error": + return ErrConflict + case "rate_limited": + return ErrRateLimited + case "internal_server_error": + return ErrInternalServer + case "service_unavailable": + return ErrServiceUnavailable + default: + return fmt.Errorf("notion: unknown error (%v)", err.Code) + } +} + +func parseErrorResponse(res *http.Response) error { + var apiErr APIError + + err := json.NewDecoder(res.Body).Decode(&apiErr) + if err != nil { + return fmt.Errorf("failed to parse error from HTTP response: %w", err) + } + + return &apiErr +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0aa2416 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/dstotijn/go-notion + +go 1.16 diff --git a/rich_text.go b/rich_text.go new file mode 100644 index 0000000..b6c44b8 --- /dev/null +++ b/rich_text.go @@ -0,0 +1,58 @@ +package notion + +import "time" + +type RichText struct { + PlainText string `json:"plain_text"` + HRef *string `json:"href"` + Annotations Annotations `json:"annotations"` + Type string `json:"type"` + + Text *Text `json:"text"` + Mention *Mention `json:"mention"` + Equation *Equation `json:"equation"` +} + +type Equation struct { + Expression string `json:"expression"` +} + +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"` +} + +type Mention struct { + Type string `json:"type"` + + User *User `json:"user"` + Page *PageMention `json:"page"` + Database *DatabaseMention `json:"database"` + Date *Date `json:"date"` +} + +type Date struct { + Start time.Time `json:"start"` + End *time.Time `json:"end"` +} + +type Text struct { + Content string `json:"content"` + Link *Link `json:"link"` +} + +type Link struct { + URL string `json:"url"` +} + +type PageMention struct { + ID string `json:"id"` +} + +type DatabaseMention struct { + ID string `json:"id"` +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..ff34c4c --- /dev/null +++ b/user.go @@ -0,0 +1,17 @@ +package notion + +type Person struct { + Email string `json:"email"` +} + +type Bot struct{} + +type User struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + AvatarURL *string `json:"avatar_url"` + + Person *Person `json:"person"` + Bot *Bot `json:"bot"` +}