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;