From e38c0c5461229a4e1e3e4fe42999d50fb3a73aa9 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Tue, 7 Feb 2023 09:43:15 +0100 Subject: [PATCH 01/10] Use UUIDv4 for IDs instead of String --- Cargo.toml | 9 +++++++++ src/ids.rs | 53 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e19132..dd34c97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,15 @@ features = ["full"] version = "1.0" features = ["derive"] +[dependencies.uuid] +version = "1.2.2" +features = [ + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs + "serde", +] + [dev-dependencies] cargo-husky = "1" wiremock = "0.5.2" diff --git a/src/ids.rs b/src/ids.rs index beffd06..f869f0f 100644 --- a/src/ids.rs +++ b/src/ids.rs @@ -1,8 +1,9 @@ use std::fmt::Display; use std::fmt::Error; +use uuid::Uuid; pub trait Identifier: Display { - fn value(&self) -> &str; + fn value(&self) -> String; } /// Meant to be a helpful trait allowing anything that can be /// identified by the type specified in `ById`. @@ -15,7 +16,7 @@ where T: Identifier, { fn as_id(&self) -> &T { - &self + self } } @@ -27,16 +28,43 @@ where self } } +macro_rules! uuid_identifer { + ($name:ident) => { + #[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq, Hash, Clone)] + #[serde(transparent)] + pub struct $name(Uuid); + + impl Identifier for $name { + fn value(&self) -> String { + self.0.to_string() + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl std::str::FromStr for $name { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok($name(Uuid::parse_str(s).unwrap())) + } + } + }; +} -macro_rules! identifer { +macro_rules! string_identifer { ($name:ident) => { #[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq, Hash, Clone)] #[serde(transparent)] pub struct $name(String); impl Identifier for $name { - fn value(&self) -> &str { - &self.0 + fn value(&self) -> String { + self.0.clone() } } @@ -50,17 +78,20 @@ macro_rules! identifer { type Err = Error; fn from_str(s: &str) -> Result { - Ok($name(s.to_string())) + Ok($name(String::from_str(s).unwrap())) } } }; } -identifer!(DatabaseId); -identifer!(PageId); -identifer!(BlockId); -identifer!(UserId); -identifer!(PropertyId); +// According to Notion API Reference id's are UUIDv4 (https://developers.notion.com/reference/intro#json-conventions) +// can be represented with or without dashes. +// Using uuid crate. +uuid_identifer!(DatabaseId); +uuid_identifer!(PageId); +uuid_identifer!(BlockId); +string_identifer!(UserId); +string_identifer!(PropertyId); impl From for BlockId { fn from(page_id: PageId) -> Self { From a068ec13814a760330f30b1c30f3c6fec3c1e444 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Tue, 7 Feb 2023 09:50:29 +0100 Subject: [PATCH 02/10] Fix Mention object - Page mention only contains id --- src/models/mod.rs | 5 +++++ src/models/text.rs | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/models/mod.rs b/src/models/mod.rs index aca6cfe..f9802f2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -180,6 +180,11 @@ impl Properties { } } +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct WeakPage { + pub id: PageId +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Page { pub id: PageId, diff --git a/src/models/text.rs b/src/models/text.rs index 4188dbf..11b6110 100644 --- a/src/models/text.rs +++ b/src/models/text.rs @@ -1,8 +1,10 @@ use crate::models::properties::DateValue; use crate::models::users::User; -use crate::{Database, Page}; +use crate::Database; use serde::{Deserialize, Serialize}; +use super::WeakPage; + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum TextColor { @@ -68,8 +70,11 @@ pub enum MentionObject { user: User, }, // TODO: need to add tests + // Page mention does not contain full Page structure, but just id + // https://developers.notion.com/reference/rich-text#page-mention-type-object + // 2023-02-07 Page { - page: Page, + page: WeakPage, }, // TODO: need to add tests Database { From 1c5bd4216f09ff338a7c39bfde3054c31dd3e7c6 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Tue, 7 Feb 2023 09:58:57 +0100 Subject: [PATCH 03/10] Add support for date_range in rollup fields --- src/models/properties.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/properties.rs b/src/models/properties.rs index 3fa5935..b64e393 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -87,6 +87,9 @@ pub struct Relation { pub synced_property_id: Option, } + +// 2023-02-07 +// below list is not complete. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum RollupFunction { @@ -104,6 +107,7 @@ pub enum RollupFunction { Max, Range, ShowOriginal, + DateRange, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -233,7 +237,7 @@ pub struct RelationValue { #[serde(tag = "type", rename_all = "snake_case")] pub enum RollupValue { Number { number: Option }, - Date { date: Option> }, + Date { date: Option }, Array { array: Vec }, } From d321d6c1caa235c7860b96a25babf9327148cc4f Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Tue, 7 Feb 2023 10:32:58 +0100 Subject: [PATCH 04/10] Add support for Timestamp filter object --- src/models/search.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/models/search.rs b/src/models/search.rs index e7d8189..11b6814 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -265,11 +265,37 @@ pub enum PropertyCondition { #[derive(Serialize, Debug, Eq, PartialEq, Clone)] pub struct FilterCondition { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(flatten)] + + + // Support FilterTimestampCondition + pub property_condtion: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(flatten)] + pub timestamp_condition: Option, +} + +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +pub struct FilterPropertyCondition { pub property: String, #[serde(flatten)] pub condition: PropertyCondition, } +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +pub struct FilterTimestampCondition { + pub timestamp: DatabaseSortTimestamp, + #[serde(flatten)] + pub condition: PropertyTimestampCondition, +} +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PropertyTimestampCondition { + CreatedTime(DateCondition), + LastEditedTime(DateCondition), +} + #[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum DatabaseSortTimestamp { From 5347964fa61d77f7c984a4f49a0a24cd0d577f76 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Tue, 7 Feb 2023 11:20:29 +0100 Subject: [PATCH 05/10] Add support for Timestamp filter condition --- src/models/search.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/models/search.rs b/src/models/search.rs index b48a12a..242661f 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -259,6 +259,8 @@ pub enum PropertyCondition { Formula(FormulaCondition), } +pub type TimestampFilterProperty = DatabaseSortTimestamp; + #[derive(Serialize, Debug, Eq, PartialEq, Clone)] #[serde(untagged)] pub enum FilterCondition { @@ -268,10 +270,9 @@ pub enum FilterCondition { condition: PropertyCondition, }, Timestamp { - timestamp: DatabaseSortTimestamp, + timestamp: TimestampFilterProperty, #[serde(flatten)] condition: PropertyTimestampCondition, - }, /// Returns pages when **all** of the filters inside the provided vector match. And { and: Vec }, @@ -317,10 +318,7 @@ pub struct DatabaseQuery { } impl Pageable for DatabaseQuery { - fn start_from( - self, - starting_point: Option, - ) -> Self { + fn start_from(self, starting_point: Option) -> Self { DatabaseQuery { paging: Some(Paging { start_cursor: starting_point, From 953ae3130543597677e79bcc9ba017b7421ab823 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Sat, 11 Mar 2023 01:06:20 +0100 Subject: [PATCH 06/10] Add rollup and select prop tests; remove unneded (previously added) WeakPage struct --- src/models/mod.rs | 5 -- src/models/properties/tests.rs | 50 ++++++++++++++++++- .../properties/tests/relation_property.json | 10 ++++ .../tests/rollup_number_property.json | 9 ++++ .../tests/select_empty_property.json | 5 ++ src/models/text.rs | 4 +- 6 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/models/properties/tests/relation_property.json create mode 100644 src/models/properties/tests/rollup_number_property.json create mode 100644 src/models/properties/tests/select_empty_property.json diff --git a/src/models/mod.rs b/src/models/mod.rs index 68ab3a1..3c9d12b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -186,11 +186,6 @@ pub struct PageCreateRequest { pub properties: Properties, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct WeakPage { - pub id: PageId -} - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Page { pub id: PageId, diff --git a/src/models/properties/tests.rs b/src/models/properties/tests.rs index 86bead8..de659f5 100644 --- a/src/models/properties/tests.rs +++ b/src/models/properties/tests.rs @@ -1,3 +1,5 @@ +use crate::models::properties::{RollupFunction, RelationValue}; + use super::{DateOrDateTime, PropertyValue, RollupPropertyValue, RollupValue}; use chrono::NaiveDate; @@ -25,6 +27,16 @@ fn parse_null_select_property() { fn parse_select_property() { let _property: PropertyValue = serde_json::from_str(include_str!("tests/select_property.json")).unwrap(); + assert!(matches!(_property, + PropertyValue::Select { select: Some(..), .. })); +} + +#[test] +fn parse_select_empty_property() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/select_empty_property.json")).unwrap(); + assert!(matches!(_property, + PropertyValue::Select { select: None, .. })); } #[test] @@ -33,6 +45,25 @@ fn parse_text_property_with_link() { serde_json::from_str(include_str!("tests/text_with_link.json")).unwrap(); } +#[test] +fn parse_relation_property() { + let property: PropertyValue = + serde_json::from_str(include_str!("tests/relation_property.json")).unwrap(); + + assert!(matches!( + property, + PropertyValue::Relation { .. } + )); + + if let PropertyValue::Relation { + relation, + .. + } = &property + { + assert!(matches!(relation[0], RelationValue{ .. })) + } +} + #[test] fn parse_rollup_property() { let property: PropertyValue = @@ -41,16 +72,31 @@ fn parse_rollup_property() { assert!(matches!( property, PropertyValue::Rollup { - rollup: Some(RollupValue::Array { .. }), + rollup: RollupValue::Array { .. }, .. } )); if let PropertyValue::Rollup { - rollup: Some(RollupValue::Array { array }), + rollup: RollupValue::Array { array , function: RollupFunction::ShowOriginal }, .. } = property { assert!(matches!(array[0], RollupPropertyValue::Text { .. })) } } + + +#[test] +fn parse_rollup_number_property() { + let property: PropertyValue = + serde_json::from_str(include_str!("tests/rollup_number_property.json")).unwrap(); + + assert!(matches!( + property, + PropertyValue::Rollup { + rollup: RollupValue::Number { .. }, + .. + } + )); +} diff --git a/src/models/properties/tests/relation_property.json b/src/models/properties/tests/relation_property.json new file mode 100644 index 0000000..c240d57 --- /dev/null +++ b/src/models/properties/tests/relation_property.json @@ -0,0 +1,10 @@ +{ + "id": "dummy_id", + "type": "relation", + "relation": [ + { + "id": "9f4b3f27-f6f1-4d83-bce5-06d131e354d3" + } + ], + "has_more": false +} \ No newline at end of file diff --git a/src/models/properties/tests/rollup_number_property.json b/src/models/properties/tests/rollup_number_property.json new file mode 100644 index 0000000..afa395d --- /dev/null +++ b/src/models/properties/tests/rollup_number_property.json @@ -0,0 +1,9 @@ +{ + "id": "WD%3Cs", + "type": "rollup", + "rollup": { + "type": "number", + "number": 42, + "function": "sum" + } + } \ No newline at end of file diff --git a/src/models/properties/tests/select_empty_property.json b/src/models/properties/tests/select_empty_property.json new file mode 100644 index 0000000..178a4ea --- /dev/null +++ b/src/models/properties/tests/select_empty_property.json @@ -0,0 +1,5 @@ +{ + "id": "D~vu", + "type": "select", + "select": null +} \ No newline at end of file diff --git a/src/models/text.rs b/src/models/text.rs index 7906532..4106290 100644 --- a/src/models/text.rs +++ b/src/models/text.rs @@ -3,7 +3,7 @@ use crate::models::users::User; use crate::Database; use serde::{Deserialize, Serialize}; -use super::WeakPage; +use super::properties::RelationValue; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "snake_case")] @@ -76,7 +76,7 @@ pub enum MentionObject { // https://developers.notion.com/reference/rich-text#page-mention-type-object // 2023-02-07 Page { - page: WeakPage, + page: RelationValue, }, // TODO: need to add tests Database { From 4454a663a7f14b727b65bf32b5df592af108f636 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Sun, 13 Aug 2023 23:34:24 +0200 Subject: [PATCH 07/10] update page --- Cargo.toml | 1 + src/lib.rs | 25 ++++++- src/models/mod.rs | 60 +++++++++++++++-- src/models/properties.rs | 139 +++++++++++++++++++++++++++++++-------- src/models/search.rs | 64 +++++++++++++----- src/models/users.rs | 3 +- 6 files changed, 241 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f80b887..44faa89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ license = "MIT" [dependencies] serde_json = "1.0" +serde_path_to_error = "0.1.14" thiserror = "1.0" tracing = "0.1" diff --git a/src/lib.rs b/src/lib.rs index dca0158..0f80282 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ use crate::models::error::ErrorResponse; use crate::models::search::{DatabaseQuery, SearchRequest}; use crate::models::{Block, Database, ListResponse, Object, Page}; use ids::{AsIdentifier, PageId}; -use models::PageCreateRequest; +use models::{PageCreateRequest, PageUpdateRequest}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{header, Client, ClientBuilder, RequestBuilder}; use tracing::Instrument; @@ -13,7 +13,7 @@ pub mod models; pub use chrono; -const NOTION_API_VERSION: &str = "2022-02-22"; +const NOTION_API_VERSION: &str = "2022-06-28"; /// An wrapper Error type for all errors produced by the [`NotionApi`](NotionApi) client. #[derive(Debug, thiserror::Error)] @@ -198,6 +198,27 @@ impl NotionApi { } } + pub async fn update_page>( + &self, + page_id: &PageId, + page_update: T, + ) -> Result { + let result = self + .make_json_request( + self.client + .patch(format!( + "https://api.notion.com/v1/pages/{}", + page_id.as_id() + )) + .json(&page_update.into()), + ) + .await?; + match result { + Object::Page { page } => Ok(page), + response => Err(Error::UnexpectedResponse { response }), + } + } + /// Query a database and return the matching pages. pub async fn query_database( &self, diff --git a/src/models/mod.rs b/src/models/mod.rs index 3c9d12b..a2d3b68 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -186,6 +186,11 @@ pub struct PageCreateRequest { pub properties: Properties, } +#[derive(Serialize, Debug, Eq, PartialEq)] +pub struct PageUpdateRequest { + pub properties: Properties, +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Page { pub id: PageId, @@ -395,12 +400,12 @@ pub struct TableOfContents { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ColumnListFields { - pub children: Vec, + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ColumnFields { - pub children: Vec, + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -411,7 +416,7 @@ pub struct LinkPreviewFields { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct TemplateFields { pub rich_text: Vec, - pub children: Vec, + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -430,7 +435,7 @@ pub struct SyncedFromObject { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct SyncedBlockFields { pub synced_from: Option, - pub children: Vec, + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -438,12 +443,12 @@ pub struct TableFields { pub table_width: u64, pub has_column_header: bool, pub has_row_header: bool, - pub children: Vec, + pub children: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct TableRowFields { - pub cells: Vec, + pub cells: Vec>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -615,6 +620,49 @@ pub enum Block { Unknown, } +impl Block { + pub fn has_children(&self) -> bool { + use Block::*; + match self { + Paragraph { common, .. } + | Heading1 { common, .. } + | Heading2 { common, .. } + | Heading3 { common, .. } + | Callout { common, .. } + | Quote { common, .. } + | BulletedListItem { common, .. } + | NumberedListItem { common, .. } + | ToDo { common, .. } + | Toggle { common, .. } + | Code { common, .. } + | ChildPage { common, .. } + | ChildDatabase { common, .. } + | Embed { common, .. } + | Image { common, .. } + | Video { common, .. } + | File { common, .. } + | Pdf { common, .. } + | Bookmark { common, .. } + | Equation { common, .. } + | Divider { common, .. } + | TableOfContents { common, .. } + | Breadcrumb { common, .. } + | ColumnList { common, .. } + | Column { common, .. } + | LinkPreview { common, .. } + | Template { common, .. } + | LinkToPage { common, .. } + | SyncedBlock { common, .. } + | Table { common, .. } + | TableRow { common, .. } + | Unsupported { common, .. } => common.has_children, + Unknown => { + panic!("Trying to reference identifier for unknown block!") + } + } + } +} + impl AsIdentifier for Block { fn as_id(&self) -> &BlockId { use Block::*; diff --git a/src/models/properties.rs b/src/models/properties.rs index 5d9a528..dc7251e 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -1,7 +1,9 @@ +use std::cmp::Ordering; + use crate::models::text::RichText; use crate::models::users::User; -use super::{DateTime, Number, Utc}; +use super::{DateTime, FileObject, Number, Utc}; use crate::ids::{DatabaseId, PageId, PropertyId}; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; @@ -234,6 +236,29 @@ pub enum DateOrDateTime { DateTime(DateTime), } +impl DateOrDateTime { + pub fn as_date(&self) -> Option<&NaiveDate> { + match self { + DateOrDateTime::Date(date) => Some(date), + _ => None, + } + } + pub fn as_date_time(&self) -> Option> { + match self { + DateOrDateTime::Date(date) => Some( + date.clone() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_local_timezone(Utc) + .unwrap(), + ), + DateOrDateTime::DateTime(date_time) => Some(*date_time), + _ => None, + } + } +} + +/// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct DateValue { pub start: DateOrDateTime, @@ -261,26 +286,47 @@ pub struct RelationValue { pub id: PageId, } +/// Notion API reference documentation is somewhat misleading +/// Rollup property value is defined on "Property values" +/// and "Page property values" #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum RollupValue { - Number { number: Option }, - Date { date: Option }, - Array { array: Vec }, + /// According to + /// One another option is: String + /// String { string: Option } + Number { + number: Option, + function: RollupFunction, + }, + + /// + /// "Date rollup property values contain a date property value within the date property." + Date { + date: Option, + function: RollupFunction, + }, + Array { + array: Vec, + function: RollupFunction, + }, } +/// FileReference, a subject of File property value, augments FileObject with name String +/// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct FileReference { pub name: String, - pub url: String, - pub mime_type: String, + #[serde(flatten)] + pub file_object: FileObject, } +/// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum PropertyValue { - // + /// Title { id: PropertyId, title: Vec, @@ -291,64 +337,75 @@ pub enum PropertyValue { id: PropertyId, rich_text: Vec, }, - /// + + /// Number { id: PropertyId, number: Option, }, - /// + + /// Select { id: PropertyId, select: Option, }, + /// Status { id: PropertyId, status: Option, }, - /// + + /// MultiSelect { id: PropertyId, - multi_select: Option>, + multi_select: Vec, }, - /// + /// Date { id: PropertyId, date: Option, }, - /// + + /// Formula { id: PropertyId, formula: FormulaResultValue, }, - /// + + /// /// It is actually an array of relations Relation { id: PropertyId, - relation: Option>, + has_more: bool, // added according to reference, but not sure if this not cause conflicts + relation: Vec, }, + /// - Rollup { - id: PropertyId, - rollup: Option, - }, - /// + Rollup { id: PropertyId, rollup: RollupValue }, + + /// People { id: PropertyId, people: Vec }, - /// + + /// Files { id: PropertyId, - files: Option>, + files: Vec, }, - /// + + /// Checkbox { id: PropertyId, checkbox: bool }, + /// Url { id: PropertyId, url: Option }, - /// + + /// Email { id: PropertyId, email: Option, }, - /// + + /// PhoneNumber { id: PropertyId, phone_number: String, @@ -372,6 +429,27 @@ pub enum PropertyValue { }, } +impl PartialOrd for PropertyValue { + fn partial_cmp( + &self, + other: &Self, + ) -> Option { + match (self, other) { + ( + &PropertyValue::Date { ref date, .. }, + &PropertyValue::Date { + date: ref date2, .. + }, + ) => Some(Ordering::Equal), + _ => None, + } + } +} + +/// TODO This is completely wrong. +/// rename to RollupArrayValueProperties +/// implement according to https://developers.notion.com/reference/page-property-values#rollup +/// /// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] @@ -382,6 +460,13 @@ pub enum RollupPropertyValue { Text { rich_text: Vec, }, + + /// according to notion-sdk-js code + #[serde(rename = "rich_text")] + Title { + rich_text: Vec, + }, + /// Number { number: Option, @@ -403,10 +488,10 @@ pub enum RollupPropertyValue { Formula { formula: FormulaResultValue, }, - /// /// It is actually an array of relations + /// Relation { - relation: Option>, + relation: Vec, }, /// Rollup { diff --git a/src/models/search.rs b/src/models/search.rs index 242661f..6462c3d 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -261,7 +261,7 @@ pub enum PropertyCondition { pub type TimestampFilterProperty = DatabaseSortTimestamp; -#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Debug, Eq, PartialEq)] #[serde(untagged)] pub enum FilterCondition { Property { @@ -270,7 +270,6 @@ pub enum FilterCondition { condition: PropertyCondition, }, Timestamp { - timestamp: TimestampFilterProperty, #[serde(flatten)] condition: PropertyTimestampCondition, }, @@ -280,11 +279,40 @@ pub enum FilterCondition { Or { or: Vec }, } -#[derive(Serialize, Debug, Eq, PartialEq, Clone)] + +impl FilterCondition { + pub fn created_time(date_condition :DateCondition) -> FilterCondition { + FilterCondition::Timestamp { condition: PropertyTimestampCondition::CreatedTime { created_time: date_condition } + } + } +} + +impl Clone for FilterCondition { + fn clone(&self) -> Self { + match self { + FilterCondition::Property { property, condition } => FilterCondition::Property { property: property.clone(), condition: condition.clone() }, + FilterCondition::Timestamp { condition } => FilterCondition::Timestamp { condition: condition.clone() }, + FilterCondition::And { and } => todo!(), + FilterCondition::Or { or } => todo!(), + } + } +} + +#[derive(Serialize, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] +#[serde(tag = "timestamp")] pub enum PropertyTimestampCondition { - CreatedTime(DateCondition), - LastEditedTime(DateCondition), + CreatedTime{created_time: DateCondition}, + LastEditedTime{last_edited_time: DateCondition}, +} + +impl PropertyTimestampCondition { + pub fn clone(&self) -> Self { + match self { + PropertyTimestampCondition::CreatedTime { created_time } => PropertyTimestampCondition::CreatedTime { created_time: created_time.clone() }, + PropertyTimestampCondition::LastEditedTime { last_edited_time } => PropertyTimestampCondition::LastEditedTime { last_edited_time: last_edited_time.clone() }, + } + } } #[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] @@ -295,16 +323,22 @@ pub enum DatabaseSortTimestamp { } #[derive(Serialize, Debug, Eq, PartialEq, Clone)] -pub struct DatabaseSort { - // Todo: Should property and timestamp be mutually exclusive? (i.e a flattened enum?) - // the documentation is not clear: - // https://developers.notion.com/reference/post-database-query#post-database-query-sort - #[serde(skip_serializing_if = "Option::is_none")] - pub property: Option, - /// The name of the timestamp to sort against. - #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp: Option, - pub direction: SortDirection, +#[serde(untagged)] +pub enum DatabaseSort { + Property { property: String, direction: SortDirection }, + Timestamp { timestamp: DatabaseSortTimestamp, direction: SortDirection } +} + +impl DatabaseSort { + pub fn by_prop(property: String, direction: SortDirection) -> DatabaseSort { + DatabaseSort::Property { property, direction } + } + pub fn by_created_time(direction: SortDirection) -> DatabaseSort { + DatabaseSort::Timestamp { timestamp: DatabaseSortTimestamp::CreatedTime, direction} + } + pub fn by_last_edited_time(direction: SortDirection) -> DatabaseSort { + DatabaseSort::Timestamp { timestamp: DatabaseSortTimestamp::LastEditedTime, direction } + } } #[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)] diff --git a/src/models/users.rs b/src/models/users.rs index b7702d0..18d66a7 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -13,9 +13,10 @@ pub struct Person { pub email: String, } +/// TODO not finished implementation of bot option +/// #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub struct Bot { - pub email: String, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] From d5a84df6add6b76d73d8575fe076532b80dcb163 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Sun, 12 Nov 2023 11:32:35 +0100 Subject: [PATCH 08/10] refine FilterCondition --- src/models/search.rs | 74 ++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/src/models/search.rs b/src/models/search.rs index 6462c3d..5bf65de 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -261,7 +261,7 @@ pub enum PropertyCondition { pub type TimestampFilterProperty = DatabaseSortTimestamp; -#[derive(Serialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] #[serde(untagged)] pub enum FilterCondition { Property { @@ -279,38 +279,37 @@ pub enum FilterCondition { Or { or: Vec }, } - impl FilterCondition { - pub fn created_time(date_condition :DateCondition) -> FilterCondition { - FilterCondition::Timestamp { condition: PropertyTimestampCondition::CreatedTime { created_time: date_condition } - } - } -} - -impl Clone for FilterCondition { - fn clone(&self) -> Self { - match self { - FilterCondition::Property { property, condition } => FilterCondition::Property { property: property.clone(), condition: condition.clone() }, - FilterCondition::Timestamp { condition } => FilterCondition::Timestamp { condition: condition.clone() }, - FilterCondition::And { and } => todo!(), - FilterCondition::Or { or } => todo!(), + pub fn created_time(date_condition: DateCondition) -> FilterCondition { + FilterCondition::Timestamp { + condition: PropertyTimestampCondition::CreatedTime { + created_time: date_condition, + }, } } } -#[derive(Serialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] #[serde(rename_all = "snake_case")] #[serde(tag = "timestamp")] pub enum PropertyTimestampCondition { - CreatedTime{created_time: DateCondition}, - LastEditedTime{last_edited_time: DateCondition}, + CreatedTime { created_time: DateCondition }, + LastEditedTime { last_edited_time: DateCondition }, } impl PropertyTimestampCondition { pub fn clone(&self) -> Self { match self { - PropertyTimestampCondition::CreatedTime { created_time } => PropertyTimestampCondition::CreatedTime { created_time: created_time.clone() }, - PropertyTimestampCondition::LastEditedTime { last_edited_time } => PropertyTimestampCondition::LastEditedTime { last_edited_time: last_edited_time.clone() }, + PropertyTimestampCondition::CreatedTime { created_time } => { + PropertyTimestampCondition::CreatedTime { + created_time: created_time.clone(), + } + } + PropertyTimestampCondition::LastEditedTime { last_edited_time } => { + PropertyTimestampCondition::LastEditedTime { + last_edited_time: last_edited_time.clone(), + } + } } } } @@ -325,19 +324,37 @@ pub enum DatabaseSortTimestamp { #[derive(Serialize, Debug, Eq, PartialEq, Clone)] #[serde(untagged)] pub enum DatabaseSort { - Property { property: String, direction: SortDirection }, - Timestamp { timestamp: DatabaseSortTimestamp, direction: SortDirection } + Property { + property: String, + direction: SortDirection, + }, + Timestamp { + timestamp: DatabaseSortTimestamp, + direction: SortDirection, + }, } impl DatabaseSort { - pub fn by_prop(property: String, direction: SortDirection) -> DatabaseSort { - DatabaseSort::Property { property, direction } + pub fn by_prop( + property: String, + direction: SortDirection, + ) -> DatabaseSort { + DatabaseSort::Property { + property, + direction, + } } pub fn by_created_time(direction: SortDirection) -> DatabaseSort { - DatabaseSort::Timestamp { timestamp: DatabaseSortTimestamp::CreatedTime, direction} + DatabaseSort::Timestamp { + timestamp: DatabaseSortTimestamp::CreatedTime, + direction, + } } pub fn by_last_edited_time(direction: SortDirection) -> DatabaseSort { - DatabaseSort::Timestamp { timestamp: DatabaseSortTimestamp::LastEditedTime, direction } + DatabaseSort::Timestamp { + timestamp: DatabaseSortTimestamp::LastEditedTime, + direction, + } } } @@ -352,7 +369,10 @@ pub struct DatabaseQuery { } impl Pageable for DatabaseQuery { - fn start_from(self, starting_point: Option) -> Self { + fn start_from( + self, + starting_point: Option, + ) -> Self { DatabaseQuery { paging: Some(Paging { start_cursor: starting_point, From 0e076505243bc62a2962a7b94d8cdfea54405e59 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Sun, 12 Nov 2023 12:12:00 +0100 Subject: [PATCH 09/10] feat paging for block query --- .gitignore | 7 +++++++ src/lib.rs | 29 +++++++++++++++++++++++++++-- src/models/paging.rs | 9 +++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 47a0031..17a0c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ Cargo.lock .api_token todo_config.toml + + + + + +# my own tests; do never merge it with remote +wowa \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0f80282..e490b75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use crate::models::error::ErrorResponse; use crate::models::search::{DatabaseQuery, SearchRequest}; use crate::models::{Block, Database, ListResponse, Object, Page}; use ids::{AsIdentifier, PageId}; +use models::paging::Paging; use models::{PageCreateRequest, PageUpdateRequest}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{header, Client, ClientBuilder, RequestBuilder}; @@ -11,6 +12,9 @@ use tracing::Instrument; pub mod ids; pub mod models; +// NEVER COMMIT THIS LINE TO REPOSITORY +pub mod wowa; + pub use chrono; const NOTION_API_VERSION: &str = "2022-06-28"; @@ -95,12 +99,17 @@ impl NotionApi { .await .map_err(|source| Error::ResponseIoError { source })?; - tracing::debug!("JSON Response: {}", json); + // tracing::trace!("JSON Response: {}", json); #[cfg(test)] { dbg!(serde_json::from_str::(&json) .map_err(|source| Error::JsonParseError { source })?); } + let deserializer = &mut serde_json::Deserializer::from_str(&json); + let result: Result = serde_path_to_error::deserialize(deserializer); + if let Err(e) = result { + panic!("{} . path: {}", e, e.path().to_string()); + } let result = serde_json::from_str(&json).map_err(|source| Error::JsonParseError { source })?; @@ -248,10 +257,26 @@ impl NotionApi { pub async fn get_block_children>( &self, block_id: T, + paging: Paging, ) -> Result, Error> { + let query = { + [ + paging.start_cursor.map(|s| format!("start_cursor={}", s)), + paging.page_size.map(|p| format!("page_size={}", p)), + ] + .iter() + .filter_map(|i| i.clone()) + .collect::>() + .join("&") + }; + let query = if query.is_empty() { + query + } else { + format!("?{query}") + }; let result = self .make_json_request(self.client.get(&format!( - "https://api.notion.com/v1/blocks/{block_id}/children", + "https://api.notion.com/v1/blocks/{block_id}/children{query}", block_id = block_id.as_id() ))) .await?; diff --git a/src/models/paging.rs b/src/models/paging.rs index c38ae11..9cf29ae 100644 --- a/src/models/paging.rs +++ b/src/models/paging.rs @@ -4,6 +4,15 @@ use serde::{Deserialize, Serialize}; #[serde(transparent)] pub struct PagingCursor(String); +impl std::fmt::Display for PagingCursor { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + f.write_str(&self.0) + } +} + #[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)] pub struct Paging { #[serde(skip_serializing_if = "Option::is_none")] From 2a3dcb38e0e39ff0b2b225fa6b7f57c67e269844 Mon Sep 17 00:00:00 2001 From: Wojciech Worwa Date: Sun, 12 Nov 2023 12:13:15 +0100 Subject: [PATCH 10/10] stisfy clippy --- src/lib.rs | 5 +---- src/models/properties.rs | 18 ------------------ src/models/search.rs | 29 ++++++----------------------- 3 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e490b75..cf62940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,9 +12,6 @@ use tracing::Instrument; pub mod ids; pub mod models; -// NEVER COMMIT THIS LINE TO REPOSITORY -pub mod wowa; - pub use chrono; const NOTION_API_VERSION: &str = "2022-06-28"; @@ -108,7 +105,7 @@ impl NotionApi { let deserializer = &mut serde_json::Deserializer::from_str(&json); let result: Result = serde_path_to_error::deserialize(deserializer); if let Err(e) = result { - panic!("{} . path: {}", e, e.path().to_string()); + panic!("{} . path: {}", e, e.path()); } let result = serde_json::from_str(&json).map_err(|source| Error::JsonParseError { source })?; diff --git a/src/models/properties.rs b/src/models/properties.rs index dc7251e..5798409 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -1,4 +1,3 @@ -use std::cmp::Ordering; use crate::models::text::RichText; use crate::models::users::User; @@ -253,7 +252,6 @@ impl DateOrDateTime { .unwrap(), ), DateOrDateTime::DateTime(date_time) => Some(*date_time), - _ => None, } } } @@ -429,22 +427,6 @@ pub enum PropertyValue { }, } -impl PartialOrd for PropertyValue { - fn partial_cmp( - &self, - other: &Self, - ) -> Option { - match (self, other) { - ( - &PropertyValue::Date { ref date, .. }, - &PropertyValue::Date { - date: ref date2, .. - }, - ) => Some(Ordering::Equal), - _ => None, - } - } -} /// TODO This is completely wrong. /// rename to RollupArrayValueProperties diff --git a/src/models/search.rs b/src/models/search.rs index 5bf65de..3c796dc 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -297,23 +297,6 @@ pub enum PropertyTimestampCondition { LastEditedTime { last_edited_time: DateCondition }, } -impl PropertyTimestampCondition { - pub fn clone(&self) -> Self { - match self { - PropertyTimestampCondition::CreatedTime { created_time } => { - PropertyTimestampCondition::CreatedTime { - created_time: created_time.clone(), - } - } - PropertyTimestampCondition::LastEditedTime { last_edited_time } => { - PropertyTimestampCondition::LastEditedTime { - last_edited_time: last_edited_time.clone(), - } - } - } - } -} - #[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum DatabaseSortTimestamp { @@ -452,7 +435,7 @@ mod tests { #[test] fn text_property_equals() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::Property { + let json = serde_json::to_value(FilterCondition::Property { property: "Name".to_string(), condition: RichText(TextCondition::Equals("Test".to_string())), })?; @@ -466,7 +449,7 @@ mod tests { #[test] fn text_property_contains() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::Property { + let json = serde_json::to_value(FilterCondition::Property { property: "Name".to_string(), condition: RichText(TextCondition::Contains("Test".to_string())), })?; @@ -480,7 +463,7 @@ mod tests { #[test] fn text_property_is_empty() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::Property { + let json = serde_json::to_value(FilterCondition::Property { property: "Name".to_string(), condition: RichText(TextCondition::IsEmpty), })?; @@ -494,7 +477,7 @@ mod tests { #[test] fn text_property_is_not_empty() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::Property { + let json = serde_json::to_value(FilterCondition::Property { property: "Name".to_string(), condition: RichText(TextCondition::IsNotEmpty), })?; @@ -508,7 +491,7 @@ mod tests { #[test] fn compound_query_and() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::And { + let json = serde_json::to_value(FilterCondition::And { and: vec![ FilterCondition::Property { property: "Seen".to_string(), @@ -535,7 +518,7 @@ mod tests { #[test] fn compound_query_or() -> Result<(), Box> { - let json = serde_json::to_value(&FilterCondition::Or { + let json = serde_json::to_value(FilterCondition::Or { or: vec![ FilterCondition::Property { property: "Description".to_string(),