Skip to content

Commit 68564b5

Browse files
committed
✨ Add Notion plugin
1 parent 9fef0c0 commit 68564b5

18 files changed

+1726
-0
lines changed

plugins/notion/.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
devManifest.json
2+
*.out
3+
*.log
4+
5+
dist/

plugins/notion/.goreleaser.yaml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This is an example .goreleaser.yml file with some sensible defaults.
2+
# Make sure to check the documentation at https://goreleaser.com
3+
4+
# The lines below are called `modelines`. See `:help modeline`
5+
# Feel free to remove those if you don't want/need to use them.
6+
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7+
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
8+
9+
version: 2
10+
11+
before:
12+
hooks:
13+
# You may remove this if you don't use go modules.
14+
- go mod tidy
15+
16+
builds:
17+
- env:
18+
- CGO_ENABLED=0
19+
goos:
20+
- linux
21+
- windows
22+
- darwin
23+
24+
goarch:
25+
- amd64
26+
- arm64
27+
28+
archives:
29+
- format: binary
30+
31+
changelog:
32+
sort: asc
33+
filters:
34+
exclude:
35+
- "^docs:"
36+
- "^test:"

plugins/notion/LICENSE

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# LICENSE
2+
3+
Copyright © 2024 Julien CAGNIART
4+

plugins/notion/README.md

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Notion plugin
2+
3+
This plugin allows you to interact with Notion databases. You can read/insert/update/delete records in a database.
4+
5+
## Installation
6+
7+
You need [Anyquery](https://github.com/julien040/anyquery) to run this plugin.
8+
9+
Then, install the plugin with the following command:
10+
11+
```bash
12+
anyquery install notion
13+
```
14+
15+
At some point, you will be asked to provide your Notion API key. You can find it by creating an integration.
16+
17+
### Find your Notion API key
18+
19+
1. Go to [Notion's My Integrations page](https://www.notion.so/my-integrations).
20+
2. Click on the `+ New integration` button.
21+
22+
![Home of Notion integrations](https://github.com/julien040/anyquery/blob/main/plugins/notion/images/creator-profile.png)
23+
3. Fill in the form with the following information:
24+
1. Name: Whatever you want
25+
2. Associated workspace: The workspace you want the plugin to have access to
26+
3. Type: Internal
27+
28+
![A form to create a new integration](https://github.com/julien040/anyquery/blob/main/plugins/notion/images/form-integration.png)
29+
4. Click on the `Save` button and on `Configure integration settings`.
30+
31+
![alt text](https://github.com/julien040/anyquery/blob/main/plugins/notion/images/success.png)
32+
5. Copy the `token` and paste it when asked by the plugin.
33+
34+
![alt text](https://github.com/julien040/anyquery/blob/main/plugins/notion/images/token.png)
35+
36+
### Finding the database ID
37+
38+
Once you have your API key, you need to find the database ID of the database you want to interact with. You can find it in the URL of the database. For example, if the URL of the database is `https://www.notion.so/myworkspace/My-Database-1234567890abcdef1234567890abcdef`, the database ID is `1234567890abcdef1234567890abcdef`.
39+
40+
## Usage
41+
42+
The plugin supports all the basic SQL operations. Here are some examples:
43+
44+
```sql
45+
SELECT * FROM notion_database;
46+
47+
SELECT * FROM notion_database WHERE name = 'Michael';
48+
49+
INSERT INTO notion_database (name, age) VALUES ('Michael', 25);
50+
51+
UPDATE notion_database SET age = 26 WHERE name = 'Michael';
52+
53+
DELETE FROM notion_database WHERE name = 'Michael';
54+
```
55+
56+
## Known limitations
57+
58+
- Rollup and UniqueID properties are not supported.
59+
- Due to the nature of formulas, a column can have different types depending on the row. This can lead to unexpected results when filtering records.
60+
- Because SQLite does not support arrays, the plugin will return a JSON representation of the array. For example, `["a", "b", "c"]` will be returned as `'["a", "b", "c"]'`. <br>
61+
You can then use the [JSON operator](https://www.sqlite.org/json1.html#the_and_operators) like in PostgreSQL to query the data. For example, `SELECT "Files & media" ->> '$[0]' FROM notion_database;` will return the first element of the array.
62+
- You cannot create/update files, formulas, or rollup properties. You cannot update the cover and icon properties of a page.
63+
- `DELETE FROM` operations only trash the record. You can restore it from the Notion interface.
64+
- Because SQLite does not have a `BOOLEAN` type, the plugin will return `0` for `false` and `1` for `true`.
65+
- Dates are returned as strings in the format `YYYY-MM-DDTHH:MM:SSZ`(RFC3339). If an end date is specified, it will be returned as a string in the format `YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ`. <br>
66+
When inserting/updating a date, you can specify the date as YYYY-MM-DD, DD/MM/YYYY, RFC3339, or a Unix timestamp. If you want to specify a time, you can use the format `YYYY-MM-DDTHH:MM:SSZ`.
67+
- Rate limit: Notion has a rate limit of 3 requests per second. While the plugin automatically handles retries, it may slow down the execution of your queries.
68+
For example, if you run a query that inserts 100 records, it will take at least 33 seconds to complete. And you will read at most 300 records per second.

plugins/notion/database_delete.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/jomei/notionapi"
8+
)
9+
10+
func (t *table) Delete(primaryKeys []interface{}) error {
11+
for _, pk := range primaryKeys {
12+
primaryKey, ok := pk.(string)
13+
if !ok {
14+
return fmt.Errorf("invalid page id: %v", pk)
15+
}
16+
17+
_, err := t.client.Page.Update(context.Background(), notionapi.PageID(primaryKey), &notionapi.PageUpdateRequest{
18+
Archived: true,
19+
Properties: map[string]notionapi.Property{},
20+
})
21+
if err != nil {
22+
return err
23+
}
24+
}
25+
return nil
26+
}

plugins/notion/database_insert.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"net/url"
7+
8+
"github.com/jomei/notionapi"
9+
)
10+
11+
func (t *table) Insert(rows [][]interface{}) error {
12+
for _, row := range rows {
13+
pageRequest := &notionapi.PageCreateRequest{
14+
Properties: map[string]notionapi.Property{},
15+
Parent: notionapi.Parent{
16+
Type: notionapi.ParentTypeDatabaseID,
17+
DatabaseID: notionapi.DatabaseID(t.database.ID),
18+
},
19+
}
20+
21+
for i, colName := range t.columns {
22+
// Skip the system columns
23+
if colName == "_page_id" || colName == "_page_url" ||
24+
colName == "_created_time" || colName == "_last_edited_time" {
25+
continue
26+
}
27+
28+
if colName == "_icon_url" || colName == "_cover_url" {
29+
// Check if the value is an URL
30+
if i < len(row) {
31+
value, ok := row[i].(string)
32+
if !ok {
33+
log.Printf("Invalid icon URL: %+v", row[i])
34+
continue
35+
}
36+
parsed, err := url.Parse(value)
37+
if err != nil {
38+
log.Printf("Invalid icon URL: %+v", row[i])
39+
continue
40+
}
41+
if parsed.Scheme == "" || parsed.Host == "" {
42+
log.Printf("Invalid icon URL: %+v", row[i])
43+
continue
44+
}
45+
if colName == "_icon_url" {
46+
pageRequest.Icon = &notionapi.Icon{
47+
Type: notionapi.FileTypeExternal,
48+
External: &notionapi.FileObject{
49+
URL: value,
50+
},
51+
}
52+
} else if colName == "_cover_url" {
53+
pageRequest.Cover = &notionapi.Image{
54+
Type: notionapi.FileTypeExternal,
55+
External: &notionapi.FileObject{
56+
URL: value,
57+
},
58+
}
59+
}
60+
61+
}
62+
}
63+
64+
// Get the property of the column
65+
prop, ok := t.database.Properties[colName]
66+
if !ok {
67+
continue
68+
}
69+
70+
// Get the value of the column
71+
var value interface{}
72+
if i < len(row) {
73+
value = row[i]
74+
} else {
75+
value = nil
76+
}
77+
78+
// Convert the value to a Notion property
79+
propValue := marshal(value, prop)
80+
if propValue == nil {
81+
continue
82+
}
83+
pageRequest.Properties[colName] = propValue
84+
85+
}
86+
// Create the page
87+
_, err := t.client.Page.Create(context.Background(), pageRequest)
88+
if err != nil {
89+
return err
90+
}
91+
92+
}
93+
94+
return nil
95+
}

0 commit comments

Comments
 (0)