notion_api_client/models/
search.rs

1use crate::ids::{PageId, UserId};
2use crate::models::paging::{Pageable, Paging, PagingCursor};
3use crate::models::Number;
4use chrono::{DateTime, Utc};
5use serde::ser::SerializeMap;
6use serde::{Serialize, Serializer};
7
8#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
9#[serde(rename_all = "snake_case")]
10pub enum SortDirection {
11    Ascending,
12    Descending,
13}
14
15#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
16#[serde(rename_all = "snake_case")]
17pub enum SortTimestamp {
18    LastEditedTime,
19}
20
21#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
22#[serde(rename_all = "snake_case")]
23pub enum FilterValue {
24    Page,
25    Database,
26}
27
28#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
29#[serde(rename_all = "snake_case")]
30pub enum FilterProperty {
31    Object,
32}
33
34#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
35pub struct Sort {
36    /// The name of the timestamp to sort against.
37    timestamp: SortTimestamp,
38    direction: SortDirection,
39}
40
41#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
42pub struct Filter {
43    property: FilterProperty,
44    value: FilterValue,
45}
46
47#[derive(Serialize, Debug, Eq, PartialEq, Default)]
48pub struct SearchRequest {
49    #[serde(skip_serializing_if = "Option::is_none")]
50    query: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    sort: Option<Sort>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    filter: Option<Filter>,
55    #[serde(flatten)]
56    paging: Option<Paging>,
57}
58
59#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
60#[serde(rename_all = "snake_case")]
61pub enum TextCondition {
62    Equals(String),
63    DoesNotEqual(String),
64    Contains(String),
65    DoesNotContain(String),
66    StartsWith(String),
67    EndsWith(String),
68    #[serde(serialize_with = "serialize_to_true")]
69    IsEmpty,
70    #[serde(serialize_with = "serialize_to_true")]
71    IsNotEmpty,
72}
73
74fn serialize_to_true<S>(serializer: S) -> Result<S::Ok, S::Error>
75where
76    S: Serializer,
77{
78    serializer.serialize_bool(true)
79}
80
81fn serialize_to_empty_object<S>(serializer: S) -> Result<S::Ok, S::Error>
82where
83    S: Serializer,
84{
85    // Todo: there has to be a better way?
86    serializer.serialize_map(Some(0))?.end()
87}
88
89#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
90#[serde(rename_all = "snake_case")]
91pub enum NumberCondition {
92    Equals(Number),
93    DoesNotEqual(Number),
94    GreaterThan(Number),
95    LessThan(Number),
96    GreaterThanOrEqualTo(Number),
97    LessThanOrEqualTo(Number),
98    #[serde(serialize_with = "serialize_to_true")]
99    IsEmpty,
100    #[serde(serialize_with = "serialize_to_true")]
101    IsNotEmpty,
102}
103
104#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
105#[serde(rename_all = "snake_case")]
106pub enum CheckboxCondition {
107    Equals(bool),
108    DoesNotEqual(bool),
109}
110
111#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
112#[serde(rename_all = "snake_case")]
113pub enum SelectCondition {
114    /// Only return pages where the page property value matches the provided value exactly.
115    Equals(String),
116    /// Only return pages where the page property value does not match the provided value exactly.
117    DoesNotEqual(String),
118    /// Only return pages where the page property value is empty.
119    #[serde(serialize_with = "serialize_to_true")]
120    IsEmpty,
121    /// Only return pages where the page property value is present.
122    #[serde(serialize_with = "serialize_to_true")]
123    IsNotEmpty,
124}
125
126#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
127#[serde(rename_all = "snake_case")]
128pub enum MultiSelectCondition {
129    /// Only return pages where the page property value contains the provided value.
130    Contains(String),
131    /// Only return pages where the page property value does not contain the provided value.
132    DoesNotContain(String),
133    /// Only return pages where the page property value is empty.
134    #[serde(serialize_with = "serialize_to_true")]
135    IsEmpty,
136    /// Only return pages where the page property value is present.
137    #[serde(serialize_with = "serialize_to_true")]
138    IsNotEmpty,
139}
140
141#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
142#[serde(rename_all = "snake_case")]
143pub enum DateCondition {
144    /// Only return pages where the page property value matches the provided date exactly.
145    /// Note that the comparison is done against the date.
146    /// Any time information sent will be ignored.
147    Equals(DateTime<Utc>),
148    /// Only return pages where the page property value is before the provided date.
149    /// Note that the comparison is done against the date.
150    /// Any time information sent will be ignored.
151    Before(DateTime<Utc>),
152    /// Only return pages where the page property value is after the provided date.
153    /// Note that the comparison is done against the date.
154    /// Any time information sent will be ignored.
155    After(DateTime<Utc>),
156    /// Only return pages where the page property value is on or before the provided date.
157    /// Note that the comparison is done against the date.
158    /// Any time information sent will be ignored.
159    OnOrBefore(DateTime<Utc>),
160    /// Only return pages where the page property value is on or after the provided date.
161    /// Note that the comparison is done against the date.
162    /// Any time information sent will be ignored.
163    OnOrAfter(DateTime<Utc>),
164    /// Only return pages where the page property value is empty.
165    #[serde(serialize_with = "serialize_to_true")]
166    IsEmpty,
167    /// Only return pages where the page property value is present.
168    #[serde(serialize_with = "serialize_to_true")]
169    IsNotEmpty,
170    /// Only return pages where the page property value is within the past week.
171    #[serde(serialize_with = "serialize_to_empty_object")]
172    PastWeek,
173    /// Only return pages where the page property value is within the past month.
174    #[serde(serialize_with = "serialize_to_empty_object")]
175    PastMonth,
176    /// Only return pages where the page property value is within the past year.
177    #[serde(serialize_with = "serialize_to_empty_object")]
178    PastYear,
179    /// Only return pages where the page property value is within the next week.
180    #[serde(serialize_with = "serialize_to_empty_object")]
181    NextWeek,
182    /// Only return pages where the page property value is within the next month.
183    #[serde(serialize_with = "serialize_to_empty_object")]
184    NextMonth,
185    /// Only return pages where the page property value is within the next year.
186    #[serde(serialize_with = "serialize_to_empty_object")]
187    NextYear,
188}
189
190#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
191#[serde(rename_all = "snake_case")]
192pub enum PeopleCondition {
193    Contains(UserId),
194    /// Only return pages where the page property value does not contain the provided value.
195    DoesNotContain(UserId),
196    /// Only return pages where the page property value is empty.
197    #[serde(serialize_with = "serialize_to_true")]
198    IsEmpty,
199    /// Only return pages where the page property value is present.
200    #[serde(serialize_with = "serialize_to_true")]
201    IsNotEmpty,
202}
203
204#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
205#[serde(rename_all = "snake_case")]
206pub enum FilesCondition {
207    /// Only return pages where the page property value is empty.
208    #[serde(serialize_with = "serialize_to_true")]
209    IsEmpty,
210    /// Only return pages where the page property value is present.
211    #[serde(serialize_with = "serialize_to_true")]
212    IsNotEmpty,
213}
214
215#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
216#[serde(rename_all = "snake_case")]
217pub enum RelationCondition {
218    /// Only return pages where the page property value contains the provided value.
219    Contains(PageId),
220    /// Only return pages where the page property value does not contain the provided value.
221    DoesNotContain(PageId),
222    /// Only return pages where the page property value is empty.
223    #[serde(serialize_with = "serialize_to_true")]
224    IsEmpty,
225    /// Only return pages where the page property value is present.
226    #[serde(serialize_with = "serialize_to_true")]
227    IsNotEmpty,
228}
229
230#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
231#[serde(rename_all = "snake_case")]
232pub enum FormulaCondition {
233    /// Only return pages where the result type of the page property formula is "text"
234    /// and the provided text filter condition matches the formula's value.
235    Text(TextCondition),
236    /// Only return pages where the result type of the page property formula is "number"
237    /// and the provided number filter condition matches the formula's value.
238    Number(NumberCondition),
239    /// Only return pages where the result type of the page property formula is "checkbox"
240    /// and the provided checkbox filter condition matches the formula's value.
241    Checkbox(CheckboxCondition),
242    /// Only return pages where the result type of the page property formula is "date"
243    /// and the provided date filter condition matches the formula's value.
244    Date(DateCondition),
245}
246
247#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
248#[serde(rename_all = "snake_case")]
249pub enum PropertyCondition {
250    RichText(TextCondition),
251    Number(NumberCondition),
252    Checkbox(CheckboxCondition),
253    Select(SelectCondition),
254    MultiSelect(MultiSelectCondition),
255    Date(DateCondition),
256    People(PeopleCondition),
257    Files(FilesCondition),
258    Relation(RelationCondition),
259    Formula(FormulaCondition),
260}
261
262#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
263#[serde(untagged)]
264pub enum FilterCondition {
265    Property {
266        property: String,
267        #[serde(flatten)]
268        condition: PropertyCondition,
269    },
270    /// Returns pages when **all** of the filters inside the provided vector match.
271    And { and: Vec<FilterCondition> },
272    /// Returns pages when **any** of the filters inside the provided vector match.
273    Or { or: Vec<FilterCondition> },
274}
275
276#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
277#[serde(rename_all = "snake_case")]
278pub enum DatabaseSortTimestamp {
279    CreatedTime,
280    LastEditedTime,
281}
282
283#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
284pub struct DatabaseSort {
285    // Todo: Should property and timestamp be mutually exclusive? (i.e a flattened enum?)
286    //  the documentation is not clear:
287    //  https://developers.notion.com/reference/post-database-query#post-database-query-sort
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub property: Option<String>,
290    /// The name of the timestamp to sort against.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub timestamp: Option<DatabaseSortTimestamp>,
293    pub direction: SortDirection,
294}
295
296#[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)]
297pub struct DatabaseQuery {
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub sorts: Option<Vec<DatabaseSort>>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub filter: Option<FilterCondition>,
302    #[serde(flatten)]
303    pub paging: Option<Paging>,
304}
305
306impl Pageable for DatabaseQuery {
307    fn start_from(
308        self,
309        starting_point: Option<PagingCursor>,
310    ) -> Self {
311        DatabaseQuery {
312            paging: Some(Paging {
313                start_cursor: starting_point,
314                page_size: self.paging.and_then(|p| p.page_size),
315            }),
316            ..self
317        }
318    }
319}
320
321#[derive(Debug, Eq, PartialEq)]
322pub enum NotionSearch {
323    /// When supplied, limits which pages are returned by comparing the query to the page title.
324    Query(String),
325    /// When supplied, sorts the results based on the provided criteria.
326    ///
327    /// Limitation: Currently only a single sort is allowed and is limited to `last_edited_time`
328    Sort {
329        timestamp: SortTimestamp,
330        direction: SortDirection,
331    },
332    /// When supplied, filters the results based on the provided criteria.
333    ///
334    /// Limitation: Currently the only filter allowed is `object` which will filter by type of object (either page or database)
335    Filter {
336        /// The name of the property to filter by.
337        /// Currently the only property you can filter by is the `object` type.
338        property: FilterProperty,
339        /// The value of the property to filter the results by.
340        /// Possible values for object type include `page` or `database`.
341        value: FilterValue,
342    },
343}
344
345impl NotionSearch {
346    pub fn filter_by_databases() -> Self {
347        Self::Filter {
348            property: FilterProperty::Object,
349            value: FilterValue::Database,
350        }
351    }
352}
353
354impl From<NotionSearch> for SearchRequest {
355    fn from(search: NotionSearch) -> Self {
356        match search {
357            NotionSearch::Query(query) => SearchRequest {
358                query: Some(query),
359                ..Default::default()
360            },
361            NotionSearch::Sort {
362                direction,
363                timestamp,
364            } => SearchRequest {
365                sort: Some(Sort {
366                    timestamp,
367                    direction,
368                }),
369                ..Default::default()
370            },
371            NotionSearch::Filter { property, value } => SearchRequest {
372                filter: Some(Filter { property, value }),
373                ..Default::default()
374            },
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    mod text_filters {
382        use crate::models::search::PropertyCondition::{Checkbox, Number, RichText, Select};
383        use crate::models::search::{
384            CheckboxCondition, FilterCondition, NumberCondition, SelectCondition, TextCondition,
385        };
386        use serde_json::json;
387
388        #[test]
389        fn text_property_equals() -> Result<(), Box<dyn std::error::Error>> {
390            let json = serde_json::to_value(&FilterCondition::Property {
391                property: "Name".to_string(),
392                condition: RichText(TextCondition::Equals("Test".to_string())),
393            })?;
394            assert_eq!(
395                json,
396                json!({"property":"Name","rich_text":{"equals":"Test"}})
397            );
398
399            Ok(())
400        }
401
402        #[test]
403        fn text_property_contains() -> Result<(), Box<dyn std::error::Error>> {
404            let json = serde_json::to_value(&FilterCondition::Property {
405                property: "Name".to_string(),
406                condition: RichText(TextCondition::Contains("Test".to_string())),
407            })?;
408            assert_eq!(
409                dbg!(json),
410                json!({"property":"Name","rich_text":{"contains":"Test"}})
411            );
412
413            Ok(())
414        }
415
416        #[test]
417        fn text_property_is_empty() -> Result<(), Box<dyn std::error::Error>> {
418            let json = serde_json::to_value(&FilterCondition::Property {
419                property: "Name".to_string(),
420                condition: RichText(TextCondition::IsEmpty),
421            })?;
422            assert_eq!(
423                dbg!(json),
424                json!({"property":"Name","rich_text":{"is_empty":true}})
425            );
426
427            Ok(())
428        }
429
430        #[test]
431        fn text_property_is_not_empty() -> Result<(), Box<dyn std::error::Error>> {
432            let json = serde_json::to_value(&FilterCondition::Property {
433                property: "Name".to_string(),
434                condition: RichText(TextCondition::IsNotEmpty),
435            })?;
436            assert_eq!(
437                dbg!(json),
438                json!({"property":"Name","rich_text":{"is_not_empty":true}})
439            );
440
441            Ok(())
442        }
443
444        #[test]
445        fn compound_query_and() -> Result<(), Box<dyn std::error::Error>> {
446            let json = serde_json::to_value(&FilterCondition::And {
447                and: vec![
448                    FilterCondition::Property {
449                        property: "Seen".to_string(),
450                        condition: Checkbox(CheckboxCondition::Equals(false)),
451                    },
452                    FilterCondition::Property {
453                        property: "Yearly visitor count".to_string(),
454                        condition: Number(NumberCondition::GreaterThan(serde_json::Number::from(
455                            1000000,
456                        ))),
457                    },
458                ],
459            })?;
460            assert_eq!(
461                dbg!(json),
462                json!({"and":[
463                    {"property":"Seen","checkbox":{"equals":false}},
464                    {"property":"Yearly visitor count","number":{"greater_than":1000000}}
465                ]})
466            );
467
468            Ok(())
469        }
470
471        #[test]
472        fn compound_query_or() -> Result<(), Box<dyn std::error::Error>> {
473            let json = serde_json::to_value(&FilterCondition::Or {
474                or: vec![
475                    FilterCondition::Property {
476                        property: "Description".to_string(),
477                        condition: RichText(TextCondition::Contains("fish".to_string())),
478                    },
479                    FilterCondition::And {
480                        and: vec![
481                            FilterCondition::Property {
482                                property: "Food group".to_string(),
483                                condition: Select(SelectCondition::Equals(
484                                    "🥦Vegetable".to_string(),
485                                )),
486                            },
487                            FilterCondition::Property {
488                                property: "Is protein rich?".to_string(),
489                                condition: Checkbox(CheckboxCondition::Equals(true)),
490                            },
491                        ],
492                    },
493                ],
494            })?;
495            assert_eq!(
496                dbg!(json),
497                json!({"or":[
498                    {"property":"Description","rich_text":{"contains":"fish"}},
499                    {"and":[
500                        {"property":"Food group","select":{"equals":"🥦Vegetable"}},
501                        {"property":"Is protein rich?","checkbox":{"equals":true}}
502                    ]}
503                ]})
504            );
505
506            Ok(())
507        }
508    }
509}