notion_api_client/
lib.rs

1use crate::ids::{BlockId, DatabaseId};
2use crate::models::error::ErrorResponse;
3use crate::models::search::{DatabaseQuery, SearchRequest};
4use crate::models::{Database, ListResponse, Object, Page};
5use ids::{AsIdentifier, PageId};
6use models::block::Block;
7use models::PageCreateRequest;
8use reqwest::header::{HeaderMap, HeaderValue};
9use reqwest::{header, Client, ClientBuilder, RequestBuilder};
10use tracing::Instrument;
11
12pub mod ids;
13pub mod models;
14
15pub use chrono;
16
17const NOTION_API_VERSION: &str = "2022-02-22";
18
19/// An wrapper Error type for all errors produced by the [`NotionApi`](NotionApi) client.
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22    #[error("Invalid Notion API Token: {}", source)]
23    InvalidApiToken { source: header::InvalidHeaderValue },
24
25    #[error("Unable to build reqwest HTTP client: {}", source)]
26    ErrorBuildingClient { source: reqwest::Error },
27
28    #[error("Error sending HTTP request: {}", source)]
29    RequestFailed {
30        #[from]
31        source: reqwest::Error,
32    },
33
34    #[error("Error reading response: {}", source)]
35    ResponseIoError { source: reqwest::Error },
36
37    #[error("Error parsing json response: {}", source)]
38    JsonParseError { source: serde_json::Error },
39
40    #[error("Unexpected API Response")]
41    UnexpectedResponse { response: Object },
42
43    #[error("API Error {}({}): {}", .error.code, .error.status, .error.message)]
44    ApiError { error: ErrorResponse },
45}
46
47/// An API client for Notion.
48/// Create a client by using [new(api_token: String)](Self::new()).
49#[derive(Clone)]
50pub struct NotionApi {
51    client: Client,
52}
53
54impl NotionApi {
55    /// Creates an instance of NotionApi.
56    /// May fail if the provided api_token is an improper value.
57    pub fn new(api_token: String) -> Result<Self, Error> {
58        let mut headers = HeaderMap::new();
59        headers.insert(
60            "Notion-Version",
61            HeaderValue::from_static(NOTION_API_VERSION),
62        );
63
64        let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", api_token))
65            .map_err(|source| Error::InvalidApiToken { source })?;
66        auth_value.set_sensitive(true);
67        headers.insert(header::AUTHORIZATION, auth_value);
68
69        let client = ClientBuilder::new()
70            .default_headers(headers)
71            .build()
72            .map_err(|source| Error::ErrorBuildingClient { source })?;
73
74        Ok(Self { client })
75    }
76
77    async fn make_json_request(
78        &self,
79        request: RequestBuilder,
80    ) -> Result<Object, Error> {
81        let request = request.build()?;
82        let url = request.url();
83        tracing::trace!(
84            method = request.method().as_str(),
85            url = url.as_str(),
86            "Sending request"
87        );
88        let json = self
89            .client
90            .execute(request)
91            .instrument(tracing::trace_span!("Sending request"))
92            .await
93            .map_err(|source| Error::RequestFailed { source })?
94            .text()
95            .instrument(tracing::trace_span!("Reading response"))
96            .await
97            .map_err(|source| Error::ResponseIoError { source })?;
98
99        tracing::debug!("JSON Response: {}", json);
100        #[cfg(test)]
101        {
102            dbg!(serde_json::from_str::<serde_json::Value>(&json)
103                .map_err(|source| Error::JsonParseError { source })?);
104        }
105        let result =
106            serde_json::from_str(&json).map_err(|source| Error::JsonParseError { source })?;
107
108        match result {
109            Object::Error { error } => Err(Error::ApiError { error }),
110            response => Ok(response),
111        }
112    }
113
114    /// List all the databases shared with the supplied integration token.
115    /// > This method is apparently deprecated/"not recommended" and
116    /// > [search()](Self::search()) should be used instead.
117    pub async fn list_databases(&self) -> Result<ListResponse<Database>, Error> {
118        let builder = self.client.get("https://api.notion.com/v1/databases");
119
120        match self.make_json_request(builder).await? {
121            Object::List { list } => Ok(list.expect_databases()?),
122            response => Err(Error::UnexpectedResponse { response }),
123        }
124    }
125
126    /// Search all pages in notion.
127    /// `query` can either be a [SearchRequest] or a slightly more convenient
128    /// [NotionSearch](models::search::NotionSearch) query.
129    pub async fn search<T: Into<SearchRequest>>(
130        &self,
131        query: T,
132    ) -> Result<ListResponse<Object>, Error> {
133        let result = self
134            .make_json_request(
135                self.client
136                    .post("https://api.notion.com/v1/search")
137                    .json(&query.into()),
138            )
139            .await?;
140
141        match result {
142            Object::List { list } => Ok(list),
143            response => Err(Error::UnexpectedResponse { response }),
144        }
145    }
146
147    /// Get a database by [DatabaseId].
148    pub async fn get_database<T: AsIdentifier<DatabaseId>>(
149        &self,
150        database_id: T,
151    ) -> Result<Database, Error> {
152        let result = self
153            .make_json_request(self.client.get(format!(
154                "https://api.notion.com/v1/databases/{}",
155                database_id.as_id()
156            )))
157            .await?;
158
159        match result {
160            Object::Database { database } => Ok(database),
161            response => Err(Error::UnexpectedResponse { response }),
162        }
163    }
164
165    /// Get a page by [PageId].
166    pub async fn get_page<T: AsIdentifier<PageId>>(
167        &self,
168        page_id: T,
169    ) -> Result<Page, Error> {
170        let result = self
171            .make_json_request(self.client.get(format!(
172                "https://api.notion.com/v1/pages/{}",
173                page_id.as_id()
174            )))
175            .await?;
176
177        match result {
178            Object::Page { page } => Ok(page),
179            response => Err(Error::UnexpectedResponse { response }),
180        }
181    }
182
183    /// Creates a new page and return the created page
184    pub async fn create_page<T: Into<PageCreateRequest>>(
185        &self,
186        page: T,
187    ) -> Result<Page, Error> {
188        let result = self
189            .make_json_request(
190                self.client
191                    .post("https://api.notion.com/v1/pages")
192                    .json(&page.into()),
193            )
194            .await?;
195
196        match result {
197            Object::Page { page } => Ok(page),
198            response => Err(Error::UnexpectedResponse { response }),
199        }
200    }
201
202    /// Query a database and return the matching pages.
203    pub async fn query_database<D, T>(
204        &self,
205        database: D,
206        query: T,
207    ) -> Result<ListResponse<Page>, Error>
208    where
209        T: Into<DatabaseQuery>,
210        D: AsIdentifier<DatabaseId>,
211    {
212        let result = self
213            .make_json_request(
214                self.client
215                    .post(&format!(
216                        "https://api.notion.com/v1/databases/{database_id}/query",
217                        database_id = database.as_id()
218                    ))
219                    .json(&query.into()),
220            )
221            .await?;
222        match result {
223            Object::List { list } => Ok(list.expect_pages()?),
224            response => Err(Error::UnexpectedResponse { response }),
225        }
226    }
227
228    pub async fn get_block_children<T: AsIdentifier<BlockId>>(
229        &self,
230        block_id: T,
231    ) -> Result<ListResponse<Block>, Error> {
232        let result = self
233            .make_json_request(self.client.get(&format!(
234                "https://api.notion.com/v1/blocks/{block_id}/children",
235                block_id = block_id.as_id()
236            )))
237            .await?;
238
239        match result {
240            Object::List { list } => Ok(list.expect_blocks()?),
241            response => Err(Error::UnexpectedResponse { response }),
242        }
243    }
244}