notion_tools/
lib.rs

1//! # Notion Tools
2//!
3//! `notion-tools` is a library for interacting with the Notion API. It provides a convenient way to
4//! perform various operations such as retrieving databases, querying databases, creating pages,
5//! updating pages, archiving pages, and appending block children.
6//!
7//! ## Usage
8//!
9//! To use this library, you need to set the `NOTION_API_KEY` and `NOTION_DATABASE_ID` environment
10//! variables. The `NOTION_API_KEY` is required for authentication, while the `NOTION_DATABASE_ID`
11//! is optional and can be set later using the `database` method.
12//!
13//! ## Implemented endpoints
14//! | Endpoint | Implemented | Code |
15//! |---|:---:|---|
16//! | [Create a Token](https://developers.notion.com/reference/create-a-token) | - | |
17//! | [Append block children](https://developers.notion.com/reference/patch-block-children) | ✅ | [`Notion::append_block_children`](Notion) |
18//! | [Retrieve a block](https://developers.notion.com/reference/retrieve-a-block) | - | |
19//! | [Retrieve block children](https://developers.notion.com/reference/get-block-children) | - | |
20//! | [Update a block](https://developers.notion.com/reference/update-a-block) | - | |
21//! | [Delete a block](https://developers.notion.com/reference/delete-a-block) | - | |
22//! | [Create a page](https://developers.notion.com/reference/post-page) | ✅ | [`Notion::create_a_page`](Notion) |
23//! | [Retrieve a page](https://developers.notion.com/reference/retrieve-a-page) | - | |
24//! | [Retrieve a page property item](https://developers.notion.com/reference/retrieve-a-page-property-item) | - | |
25//! | [Update page properties](https://developers.notion.com/reference/patch-page) | ✅ | [`Notion::update_a_page`](Notion) |
26//! | [Archive a page](https://developers.notion.com/reference/archive-a-page) | ✅ | [`Notion::archive_a_page`](Notion) |
27//! | [Create a database](https://developers.notion.com/reference/create-a-database) | - | |
28//! | [Query a database](https://developers.notion.com/reference/post-database-query) | ✅ | [`Notion::query_database`](Notion) |
29//! | [Retrieve a database](https://developers.notion.com/reference/retrieve-a-database) | ✅ | [`Notion::retrieve_a_database`](Notion) |
30//! | [Update a database](https://developers.notion.com/reference/update-a-database) | - | |
31//! | [List all users](https://developers.notion.com/reference/get-users) | - | |
32//! | [Retrieve a user](https://developers.notion.com/reference/get-user) | - | |
33//! | [Retrieve your token's bot user](https://developers.notion.com/reference/get-self) | - | |
34//! | [Create comment](https://developers.notion.com/reference/create-a-comment) | - | |
35//! | [Retrieve comments](https://developers.notion.com/reference/retrieve-a-comment) | - | |
36//! | [Search by title](https://developers.notion.com/reference/post-search) | - | |
37//!
38//! ## Build a query filter
39//! The `QueryFilter` struct is used to build a query filter for querying a database. The `QueryFilter`
40//! struct provides methods for building a filter that can be used to query a database.
41//! See the [`QueryFilter`] struct for more information.
42//!
43//! ## Examples
44//!
45//! ### Create a page
46//!
47//! ```rust
48//! # use anyhow::Result;
49//! # use notion_tools::Notion;
50//! # use notion_tools::structs::page::*;
51//! # use notion_tools::structs::common::*;
52//! # use fxhash::FxHashMap;
53//!
54//! # #[tokio::main]
55//! # async fn main() -> Result<()> {
56//! let notion = Notion::new();
57//!
58//! // Create a page
59//! let mut properties: FxHashMap<String, PageProperty> = FxHashMap::default();
60//! properties.insert(
61//!     String::from("Name"),
62//!     PageProperty::title(RichText::from_str(String::from("Sample Page"))),
63//! );
64//! properties.insert(
65//!     String::from("Title"),
66//!     PageProperty::rich_text(vec![RichText::from_str(String::from("Sample Page"))]),
67//! );
68//! properties.insert(String::from("Status"), PageProperty::status(String::from("ToDo")));
69//! let mut page = Page::from_properties(properties);
70//! page.parent.type_name = ParentType::Database;
71//! page.parent.database_id = Some(notion.database_id.clone());
72//!
73//! let response = notion.create_a_page(&page).await;
74//! println!("{:?}", response);
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! ### Query a database
80//!
81//! ```rust
82//! # use anyhow::Result;
83//! # use notion_tools::Notion;
84//! # use notion_tools::structs::query_filter::*;
85//! # use notion_tools::structs::page::*;
86//! # use notion_tools::structs::common::*;
87//! #
88//! # #[tokio::main]
89//! # async fn main() -> Result<()> {
90//! let mut notion = Notion::new();
91//! notion.database(String::from("your_database_id"));
92//! # notion.database(std::env::var("NOTION_DATABASE_ID").unwrap());
93//!
94//! // Build a query filter
95//! let mut filter = QueryFilter::new();
96//! filter.args(FilterItem::status(
97//!     String::from("Status"),
98//!     StatusFilterItem::equals(String::from("ToDo")),
99//! ));
100//! // Query a database
101//! let response = notion.query_database(filter).await?;
102//! println!("{:?}", response);
103//! #     Ok(())
104//! # }
105//! ```
106//!
107pub mod structs;
108
109use crate::structs::block::*;
110use crate::structs::common::*;
111use crate::structs::database::*;
112use crate::structs::page::*;
113use crate::structs::query_filter::*;
114use anyhow::{Error, Result};
115use dotenvy::dotenv;
116use reqwest as request;
117
118/// Notion API client
119#[derive(Debug)]
120pub struct Notion {
121    /// Notion API key: set from the `NOTION_API_KEY` environment variable
122    pub api_key: String,
123    /// Notion database ID: set from the `NOTION_DATABASE_ID` environment variable
124    pub database_id: String,
125}
126
127impl Notion {
128    /// Create a new Notion API client.  
129    /// environment variables are read from the `.env` file.
130    pub fn new() -> Self {
131        dotenv().ok();
132        let api_key = std::env::var("NOTION_API_KEY").expect("NOTION_API_KEY must be set");
133        let database_id = std::env::var("NOTION_DATABASE_ID").unwrap_or("".to_string());
134
135        Notion {
136            api_key,
137            database_id,
138        }
139    }
140
141    /// Set your database ID
142    pub fn database(&mut self, database_id: String) -> &mut Self {
143        self.database_id = database_id.to_string();
144        return self;
145    }
146
147    /// # Retrieve a database properties  
148    /// ## Return
149    /// - [`Database`] struct
150    pub async fn retrieve_a_database(&self) -> Result<Database> {
151        let url = format!("https://api.notion.com/v1/databases/{}", self.database_id);
152        let client = request::Client::new();
153        let content = client
154            .get(&url)
155            .header("Content-Type", "application/json")
156            .header("Authorization", format!("Bearer {}", self.api_key))
157            .header("Notion-Version", "2022-06-28")
158            .send()
159            .await?
160            .text()
161            .await?;
162
163        let mut database = serde_json::from_str::<Database>(&content)?;
164        if database.status == 0 {
165            database.status = 200;
166        }
167
168        return Ok(database);
169    }
170
171    /// # Query a database  
172    /// ## Arguments:  
173    /// - filter: [`QueryFilter`]
174    /// ## Return:  
175    /// - [`PageResponse`] struct
176    pub async fn query_database(&self, filter: QueryFilter) -> Result<PageResponse> {
177        let url = format!(
178            "https://api.notion.com/v1/databases/{}/query",
179            self.database_id
180        );
181        let query = filter.build();
182        let client = request::Client::new();
183        let content = client
184            .post(&url)
185            .header("Content-Type", "application/json")
186            .header("Authorization", format!("Bearer {}", self.api_key))
187            .header("Notion-Version", "2022-06-28")
188            .body(query)
189            .send()
190            .await?
191            .text()
192            .await?;
193
194        let mut response = serde_json::from_str::<PageResponse>(&content)?;
195        if response.status != 0 {
196            return Err(Error::msg(
197                format!("Failed to query database: {}", response.message).to_string(),
198            ));
199        } else {
200            response.status = 200;
201        }
202        return Ok(response);
203    }
204
205    /// # Create a page
206    /// ## Arguments:
207    /// - page: [`Page`] struct
208    /// ## Return:
209    /// - [`Page`] struct
210    pub async fn create_a_page(&self, page: &Page) -> Result<Page> {
211        let url = "https://api.notion.com/v1/pages";
212        let client = request::Client::new();
213        let data = serde_json::to_string(page)?;
214        let content = client
215            .post(url)
216            .header("Content-Type", "application/json")
217            .header("Authorization", format!("Bearer {}", self.api_key))
218            .header("Notion-Version", "2022-06-28")
219            .body(data)
220            .send()
221            .await?
222            .text()
223            .await?;
224
225        let mut page = serde_json::from_str::<Page>(&content)?;
226        if page.status != 0 {
227            return Err(Error::msg(
228                format!("Failed to create page: {}", page.message).to_string(),
229            ));
230        } else {
231            page.status = 200;
232        }
233        return Ok(page);
234    }
235
236    /// # Update a page
237    /// ## Arguments:
238    /// - page_id: String
239    /// - page: [`Page`] struct
240    /// ## Return:
241    /// - [`Page`] struct
242    pub async fn update_a_page(&self, page_id: String, page: &Page) -> Result<Page> {
243        let url = format!("https://api.notion.com/v1/pages/{}", page_id);
244        let client = request::Client::new();
245        let data = serde_json::to_string(page)?;
246        let content = client
247            .patch(&url)
248            .header("Content-Type", "application/json")
249            .header("Authorization", format!("Bearer {}", self.api_key))
250            .header("Notion-Version", "2022-06-28")
251            .body(data)
252            .send()
253            .await?
254            .text()
255            .await?;
256
257        let mut page = serde_json::from_str::<Page>(&content)?;
258        if page.status != 0 {
259            return Err(Error::msg(
260                format!("Failed to update page: {}", page.message).to_string(),
261            ));
262        } else {
263            page.status = 200;
264        }
265        return Ok(page);
266    }
267
268    /// # Archive a page
269    /// ## Arguments:
270    /// - page_id: String
271    /// - parent_id: String
272    /// - parent_type: [`ParentType`]
273    /// ## Return:
274    /// - [`Page`] struct
275    /// ## Note:
276    /// - The page will be archived by updating the page with the `archived` field set to `true`.
277    pub async fn archive_a_page(
278        &self,
279        page_id: String,
280        parent_id: String,
281        parent_type: ParentType,
282    ) -> Result<Page> {
283        let mut page = Page {
284            archived: true,
285            ..Default::default()
286        };
287
288        match parent_type {
289            ParentType::Database => {
290                page.parent.type_name = parent_type;
291                page.parent.database_id = Some(parent_id.to_string());
292            }
293            ParentType::Page => {
294                page.parent.type_name = parent_type;
295                page.parent.page_id = Some(parent_id.to_string());
296            }
297            ParentType::Workspace => {
298                page.parent.type_name = parent_type;
299                page.parent.workspace_id = Some(parent_id.to_string());
300            }
301            ParentType::Block => {
302                page.parent.type_name = parent_type;
303                page.parent.block_id = Some(parent_id.to_string());
304            }
305        }
306        let page = self.update_a_page(page_id, &page).await?;
307        return Ok(page);
308    }
309
310    /// # Append block children
311    /// Because the Notion API only allows appending 100 blocks at a time, this method will split the
312    /// blocks into chunks of 100 and append them to the parent block.
313    /// ## Arguments:
314    /// - parent_id: String
315    /// - blocks: [`BlockBody`]
316    /// ## Return:
317    /// - [`BlockResponse`] struct
318    pub async fn append_block_children(
319        &self,
320        parent_id: String,
321        blocks: Vec<Block>,
322    ) -> Result<BlockResponse> {
323        let url = format!("https://api.notion.com/v1/blocks/{}/children", parent_id);
324        let client = request::Client::new();
325        let mut res_blocks: Vec<Block> = Vec::new();
326
327        for i in (0..blocks.len()).step_by(100) {
328            let end_index = std::cmp::min(i + 100, blocks.len());
329            let block_body = BlockBody {
330                children: blocks[i..end_index].to_vec(),
331            };
332            let data = serde_json::to_string(&block_body)?;
333            let content = client
334                .patch(&url)
335                .header("Content-Type", "application/json")
336                .header("Authorization", format!("Bearer {}", self.api_key))
337                .header("Notion-Version", "2022-06-28")
338                .body(data)
339                .send()
340                .await?
341                .text()
342                .await?;
343            let _bby = serde_json::from_str::<BlockResponse>(&content)?;
344
345            if _bby.status != 0 {
346                return Err(Error::msg(
347                    format!("Failed to append block children: {}", _bby.message).to_string(),
348                ));
349            } else {
350                res_blocks.extend(_bby.results);
351            }
352        }
353
354        let res_block = BlockResponse {
355            object: "list".to_string(),
356            results: res_blocks,
357            status: 200,
358            ..Default::default()
359        };
360        return Ok(res_block);
361    }
362}
363
364#[cfg(test)]
365mod tests;