notion_into_sqlite/
notion_pages.rs

1use crate::json_util::{dig_json, JsonKey};
2use crate::notion_database::{NotionDatabaseSchema, NotionPropertyType};
3use anyhow::{anyhow, Result};
4use rusqlite::ToSql;
5use serde_json::{Map, Value};
6use std::collections::HashMap;
7
8#[derive(Debug, PartialEq)]
9pub enum NotionPropertyValue {
10    Text(String),
11    Number(f64),
12    Json(Value),
13    Boolean(bool),
14}
15impl ToSql for NotionPropertyValue {
16    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
17        match self {
18            NotionPropertyValue::Text(value) => value.to_sql(),
19            NotionPropertyValue::Number(value) => value.to_sql(),
20            NotionPropertyValue::Json(value) => Ok(rusqlite::types::ToSqlOutput::from(
21                serde_json::to_string(value).unwrap(),
22            )),
23            NotionPropertyValue::Boolean(value) => value.to_sql(),
24        }
25    }
26}
27
28#[derive(Debug)]
29pub struct NotionPage {
30    pub id: String,
31    pub properties: HashMap<String, NotionPropertyValue>,
32    pub url: String,
33    pub created_time: String,
34    pub created_by: Value,
35    pub last_edited_time: String,
36    pub last_edited_by: Value,
37    pub archived: bool,
38}
39
40#[allow(non_snake_case)]
41#[derive(Debug)]
42struct NotionPageBuilder<'a> {
43    schema: &'a NotionDatabaseSchema,
44    TITLE_JSON_PATH: Vec<JsonKey<'a>>,
45    SELECT_JSON_PATH: Vec<JsonKey<'a>>,
46}
47impl NotionPageBuilder<'_> {
48    fn new(schema: &NotionDatabaseSchema) -> NotionPageBuilder<'_> {
49        NotionPageBuilder {
50            schema,
51            TITLE_JSON_PATH: vec!["title".into(), 0.into(), "plain_text".into()],
52            SELECT_JSON_PATH: vec!["select".into(), "name".into()],
53        }
54    }
55
56    fn from(&self, json_entry: &Map<String, Value>) -> Option<NotionPage> {
57        let id = json_entry.get("id")?.as_str()?.to_string();
58
59        let url = json_entry.get("url")?.as_str()?.to_string();
60        let created_time = json_entry.get("created_time")?.as_str()?.to_owned();
61        let created_by = json_entry.get("created_by")?.clone();
62        let last_edited_time = json_entry.get("last_edited_time")?.as_str()?.to_owned();
63        let last_edited_by = json_entry.get("last_edited_by")?.clone();
64        let archived = json_entry.get("archived")?.as_bool()?;
65
66        let properties_object = json_entry.get("properties")?.as_object()?;
67        let properties = properties_object
68            .iter()
69            .filter_map(|(key, property)| {
70                let property_schema = self.schema.properties.get(key)?;
71                let value: NotionPropertyValue = match property_schema.property_type {
72                    // TODO: convert to plain text
73                    NotionPropertyType::RichText => {
74                        NotionPropertyValue::Json(property.get("rich_text")?.clone())
75                    }
76                    NotionPropertyType::Number => {
77                        NotionPropertyValue::Number(property.get("number")?.as_f64()?)
78                    }
79                    NotionPropertyType::Select => NotionPropertyValue::Text(
80                        dig_json(property, &self.SELECT_JSON_PATH)?
81                            .as_str()?
82                            .to_string(),
83                    ),
84                    NotionPropertyType::Title => NotionPropertyValue::Text(
85                        dig_json(property, &self.TITLE_JSON_PATH)?
86                            .as_str()?
87                            .to_string(),
88                    ),
89                    NotionPropertyType::Checkbox => {
90                        NotionPropertyValue::Boolean(property.get("checkbox")?.as_bool()?)
91                    }
92                    NotionPropertyType::Url => {
93                        NotionPropertyValue::Text(property.get("url")?.as_str()?.to_string())
94                    }
95                    NotionPropertyType::Email => {
96                        NotionPropertyValue::Text(property.get("email")?.as_str()?.to_string())
97                    }
98                    NotionPropertyType::PhoneNumber => NotionPropertyValue::Text(
99                        property.get("phone_number")?.as_str()?.to_string(),
100                    ),
101                    NotionPropertyType::CreatedTime => NotionPropertyValue::Text(
102                        property.get("created_time")?.as_str()?.to_string(),
103                    ),
104                    NotionPropertyType::LastEditedTime => NotionPropertyValue::Text(
105                        property.get("last_edited_time")?.as_str()?.to_string(),
106                    ),
107                    NotionPropertyType::Other => NotionPropertyValue::Json(property.clone()),
108                    _ => NotionPropertyValue::Json(
109                        property.get(&property_schema.property_raw_type)?.clone(),
110                    ),
111                };
112                Some((key.to_string(), value))
113            })
114            .collect::<HashMap<String, NotionPropertyValue>>();
115
116        Some(NotionPage {
117            id,
118            properties,
119            url,
120            created_time,
121            created_by,
122            last_edited_by,
123            last_edited_time,
124            archived,
125        })
126    }
127}
128
129pub fn parse_notion_page_list(
130    schema: &NotionDatabaseSchema,
131    query_resp: &Value,
132) -> Result<(Vec<NotionPage>, Option<String>)> {
133    validate_object_type(query_resp)?;
134
135    let next_cursor = get_next_cursor(query_resp);
136
137    let results_json_keys = vec![JsonKey::String("results")];
138    let results = dig_json(query_resp, &results_json_keys)
139        .and_then(|results| results.as_array())
140        .map(|results| {
141            results
142                .iter()
143                .filter_map(|r| r.as_object())
144                .collect::<Vec<_>>()
145        })
146        .ok_or_else(|| anyhow!(r#"It must have "results" as arrray of objects."#))?;
147
148    let page_builder = NotionPageBuilder::new(schema);
149    let pages: Vec<NotionPage> = results
150        .iter()
151        .filter_map(|&result| page_builder.from(result))
152        .collect::<Vec<_>>();
153
154    Ok((pages, next_cursor))
155}
156
157fn validate_object_type(query_resp: &Value) -> Result<()> {
158    let json_keys = vec![JsonKey::String("object")];
159    let object_field = dig_json(query_resp, &json_keys)
160        .and_then(|o| o.as_str())
161        .ok_or_else(|| anyhow!(r#"It must have `"object": "list"`."#.to_string()))?;
162
163    if object_field == "list" {
164        Ok(())
165    } else {
166        Err(anyhow!(
167            r#"It must have `"object": "list"`, but was "{}""#,
168            object_field
169        ))
170    }
171}
172
173fn get_next_cursor(query_resp: &Value) -> Option<String> {
174    let json_keys: Vec<JsonKey> = vec!["next_cursor".into()];
175    Some(dig_json(query_resp, &json_keys)?.as_str()?.to_string())
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_validate_object_type() {
184        let data = r#"
185        {
186            "object": "list"
187        }
188        "#;
189        let json = serde_json::from_str(data).unwrap();
190        assert!(validate_object_type(&json).is_ok());
191
192        let data = r#"
193        {
194            "object": "xxx"
195        }
196        "#;
197        let json = serde_json::from_str(data).unwrap();
198        assert!(validate_object_type(&json).is_err());
199
200        let data = r#"
201        {}
202        "#;
203        let json = serde_json::from_str(data).unwrap();
204        assert!(validate_object_type(&json).is_err());
205    }
206}