notion_wasi/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(rename_all = "snake_case")]
264pub enum TimestampCondition {
265    CreatedTime(DateCondition),
266    LastEditedTime(DateCondition),
267}
268
269#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
270#[serde(untagged)]
271pub enum FilterCondition {
272    Property {
273        property: String,
274        #[serde(flatten)]
275        condition: PropertyCondition,
276    },
277    Timestamp {
278        timestamp: String,
279        #[serde(flatten)]
280        condition: TimestampCondition,
281    },
282    /// Returns pages when **all** of the filters inside the provided vector match.
283    And { and: Vec<FilterCondition> },
284    /// Returns pages when **any** of the filters inside the provided vector match.
285    Or { or: Vec<FilterCondition> },
286}
287
288#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
289#[serde(rename_all = "snake_case")]
290pub enum DatabaseSortTimestamp {
291    CreatedTime,
292    LastEditedTime,
293}
294
295#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
296pub struct DatabaseSort {
297    // Todo: Should property and timestamp be mutually exclusive? (i.e a flattened enum?)
298    //  the documentation is not clear:
299    //  https://developers.notion.com/reference/post-database-query#post-database-query-sort
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub property: Option<String>,
302    /// The name of the timestamp to sort against.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub timestamp: Option<DatabaseSortTimestamp>,
305    pub direction: SortDirection,
306}
307
308#[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)]
309pub struct DatabaseQuery {
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub sorts: Option<Vec<DatabaseSort>>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub filter: Option<FilterCondition>,
314    #[serde(flatten)]
315    pub paging: Option<Paging>,
316}
317
318impl Pageable for DatabaseQuery {
319    fn start_from(
320        self,
321        starting_point: Option<PagingCursor>,
322    ) -> Self {
323        DatabaseQuery {
324            paging: Some(Paging {
325                start_cursor: starting_point,
326                page_size: self.paging.and_then(|p| p.page_size),
327            }),
328            ..self
329        }
330    }
331}
332
333#[derive(Debug, Eq, PartialEq)]
334pub enum NotionSearch {
335    /// When supplied, limits which pages are returned by comparing the query to the page title.
336    Query(String),
337    /// When supplied, sorts the results based on the provided criteria.
338    ///
339    /// Limitation: Currently only a single sort is allowed and is limited to `last_edited_time`
340    Sort {
341        timestamp: SortTimestamp,
342        direction: SortDirection,
343    },
344    /// When supplied, filters the results based on the provided criteria.
345    ///
346    /// Limitation: Currently the only filter allowed is `object` which will filter by type of object (either page or database)
347    Filter {
348        /// The name of the property to filter by.
349        /// Currently the only property you can filter by is the `object` type.
350        property: FilterProperty,
351        /// The value of the property to filter the results by.
352        /// Possible values for object type include `page` or `database`.
353        value: FilterValue,
354    },
355}
356
357impl NotionSearch {
358    pub fn filter_by_databases() -> Self {
359        Self::Filter {
360            property: FilterProperty::Object,
361            value: FilterValue::Database,
362        }
363    }
364}
365
366impl From<NotionSearch> for SearchRequest {
367    fn from(search: NotionSearch) -> Self {
368        match search {
369            NotionSearch::Query(query) => SearchRequest {
370                query: Some(query),
371                ..Default::default()
372            },
373            NotionSearch::Sort {
374                direction,
375                timestamp,
376            } => SearchRequest {
377                sort: Some(Sort {
378                    timestamp,
379                    direction,
380                }),
381                ..Default::default()
382            },
383            NotionSearch::Filter { property, value } => SearchRequest {
384                filter: Some(Filter { property, value }),
385                ..Default::default()
386            },
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    mod text_filters {
394        use crate::models::search::PropertyCondition::{Checkbox, Number, RichText, Select};
395        use crate::models::search::{
396            CheckboxCondition, FilterCondition, NumberCondition, SelectCondition, TextCondition,
397        };
398        use serde_json::json;
399
400        #[test]
401        fn text_property_equals() -> Result<(), Box<dyn std::error::Error>> {
402            let json = serde_json::to_value(&FilterCondition::Property {
403                property: "Name".to_string(),
404                condition: RichText(TextCondition::Equals("Test".to_string())),
405            })?;
406            assert_eq!(
407                json,
408                json!({"property":"Name","rich_text":{"equals":"Test"}})
409            );
410
411            Ok(())
412        }
413
414        #[test]
415        fn text_property_contains() -> Result<(), Box<dyn std::error::Error>> {
416            let json = serde_json::to_value(&FilterCondition::Property {
417                property: "Name".to_string(),
418                condition: RichText(TextCondition::Contains("Test".to_string())),
419            })?;
420            assert_eq!(
421                dbg!(json),
422                json!({"property":"Name","rich_text":{"contains":"Test"}})
423            );
424
425            Ok(())
426        }
427
428        #[test]
429        fn text_property_is_empty() -> Result<(), Box<dyn std::error::Error>> {
430            let json = serde_json::to_value(&FilterCondition::Property {
431                property: "Name".to_string(),
432                condition: RichText(TextCondition::IsEmpty),
433            })?;
434            assert_eq!(
435                dbg!(json),
436                json!({"property":"Name","rich_text":{"is_empty":true}})
437            );
438
439            Ok(())
440        }
441
442        #[test]
443        fn text_property_is_not_empty() -> Result<(), Box<dyn std::error::Error>> {
444            let json = serde_json::to_value(&FilterCondition::Property {
445                property: "Name".to_string(),
446                condition: RichText(TextCondition::IsNotEmpty),
447            })?;
448            assert_eq!(
449                dbg!(json),
450                json!({"property":"Name","rich_text":{"is_not_empty":true}})
451            );
452
453            Ok(())
454        }
455
456        #[test]
457        fn compound_query_and() -> Result<(), Box<dyn std::error::Error>> {
458            let json = serde_json::to_value(&FilterCondition::And {
459                and: vec![
460                    FilterCondition::Property {
461                        property: "Seen".to_string(),
462                        condition: Checkbox(CheckboxCondition::Equals(false)),
463                    },
464                    FilterCondition::Property {
465                        property: "Yearly visitor count".to_string(),
466                        condition: Number(NumberCondition::GreaterThan(serde_json::Number::from(
467                            1000000,
468                        ))),
469                    },
470                ],
471            })?;
472            assert_eq!(
473                dbg!(json),
474                json!({"and":[
475                    {"property":"Seen","checkbox":{"equals":false}},
476                    {"property":"Yearly visitor count","number":{"greater_than":1000000}}
477                ]})
478            );
479
480            Ok(())
481        }
482
483        #[test]
484        fn compound_query_or() -> Result<(), Box<dyn std::error::Error>> {
485            let json = serde_json::to_value(&FilterCondition::Or {
486                or: vec![
487                    FilterCondition::Property {
488                        property: "Description".to_string(),
489                        condition: RichText(TextCondition::Contains("fish".to_string())),
490                    },
491                    FilterCondition::And {
492                        and: vec![
493                            FilterCondition::Property {
494                                property: "Food group".to_string(),
495                                condition: Select(SelectCondition::Equals(
496                                    "🥦Vegetable".to_string(),
497                                )),
498                            },
499                            FilterCondition::Property {
500                                property: "Is protein rich?".to_string(),
501                                condition: Checkbox(CheckboxCondition::Equals(true)),
502                            },
503                        ],
504                    },
505                ],
506            })?;
507            assert_eq!(
508                dbg!(json),
509                json!({"or":[
510                    {"property":"Description","rich_text":{"contains":"fish"}},
511                    {"and":[
512                        {"property":"Food group","select":{"equals":"🥦Vegetable"}},
513                        {"property":"Is protein rich?","checkbox":{"equals":true}}
514                    ]}
515                ]})
516            );
517
518            Ok(())
519        }
520    }
521}