mas_storage/
pagination.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7//! Utilities to manage paginated queries.
8
9use thiserror::Error;
10use ulid::Ulid;
11
12/// An error returned when invalid pagination parameters are provided
13#[derive(Debug, Error)]
14#[error("Either 'first' or 'last' must be specified")]
15pub struct InvalidPagination;
16
17/// Pagination parameters
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Pagination<Cursor = Ulid> {
20    /// The cursor to start from
21    pub before: Option<Cursor>,
22
23    /// The cursor to end at
24    pub after: Option<Cursor>,
25
26    /// The maximum number of items to return
27    pub count: usize,
28
29    /// In which direction to paginate
30    pub direction: PaginationDirection,
31}
32
33/// The direction to paginate
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum PaginationDirection {
36    /// Paginate forward
37    Forward,
38
39    /// Paginate backward
40    Backward,
41}
42
43/// A node in a page, with a cursor
44pub trait Node<C = Ulid> {
45    /// The cursor of that particular node
46    fn cursor(&self) -> C;
47}
48
49impl<C> Pagination<C> {
50    /// Creates a new [`Pagination`] from user-provided parameters.
51    ///
52    /// # Errors
53    ///
54    /// Either `first` or `last` must be provided, else this function will
55    /// return an [`InvalidPagination`] error.
56    pub fn try_new(
57        before: Option<C>,
58        after: Option<C>,
59        first: Option<usize>,
60        last: Option<usize>,
61    ) -> Result<Self, InvalidPagination> {
62        let (direction, count) = match (first, last) {
63            (Some(first), _) => (PaginationDirection::Forward, first),
64            (_, Some(last)) => (PaginationDirection::Backward, last),
65            (None, None) => return Err(InvalidPagination),
66        };
67
68        Ok(Self {
69            before,
70            after,
71            count,
72            direction,
73        })
74    }
75
76    /// Creates a [`Pagination`] which gets the first N items
77    #[must_use]
78    pub const fn first(first: usize) -> Self {
79        Self {
80            before: None,
81            after: None,
82            count: first,
83            direction: PaginationDirection::Forward,
84        }
85    }
86
87    /// Creates a [`Pagination`] which gets the last N items
88    #[must_use]
89    pub const fn last(last: usize) -> Self {
90        Self {
91            before: None,
92            after: None,
93            count: last,
94            direction: PaginationDirection::Backward,
95        }
96    }
97
98    /// Get items before the given cursor
99    #[must_use]
100    pub fn before(mut self, cursor: C) -> Self {
101        self.before = Some(cursor);
102        self
103    }
104
105    /// Clear the before cursor
106    #[must_use]
107    pub fn clear_before(mut self) -> Self {
108        self.before = None;
109        self
110    }
111
112    /// Get items after the given cursor
113    #[must_use]
114    pub fn after(mut self, cursor: C) -> Self {
115        self.after = Some(cursor);
116        self
117    }
118
119    /// Clear the after cursor
120    #[must_use]
121    pub fn clear_after(mut self) -> Self {
122        self.after = None;
123        self
124    }
125
126    /// Process a page returned by a paginated query
127    #[must_use]
128    pub fn process<T: Node<C>>(&self, mut nodes: Vec<T>) -> Page<T, C> {
129        let is_full = nodes.len() == (self.count + 1);
130        if is_full {
131            nodes.pop();
132        }
133
134        let (has_previous_page, has_next_page) = match self.direction {
135            PaginationDirection::Forward => (false, is_full),
136            PaginationDirection::Backward => {
137                // 6. If the last argument is provided, I reverse the order of the results
138                nodes.reverse();
139                (is_full, false)
140            }
141        };
142
143        let edges = nodes
144            .into_iter()
145            .map(|node| Edge {
146                cursor: node.cursor(),
147                node,
148            })
149            .collect();
150
151        Page {
152            has_next_page,
153            has_previous_page,
154            edges,
155        }
156    }
157}
158
159/// An edge in a paginated result
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct Edge<T, C = Ulid> {
162    /// The cursor of the edge
163    pub cursor: C,
164    /// The node of the edge
165    pub node: T,
166}
167
168/// A page of results returned by a paginated query
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Page<T, C = Ulid> {
171    /// When paginating forwards, this is true if there are more items after
172    pub has_next_page: bool,
173
174    /// When paginating backwards, this is true if there are more items before
175    pub has_previous_page: bool,
176
177    /// The items in the page
178    pub edges: Vec<Edge<T, C>>,
179}
180
181impl<T, C> Page<T, C> {
182    /// Map the items in this page with the given function
183    ///
184    /// # Parameters
185    ///
186    /// * `f`: The function to map the items with
187    #[must_use]
188    pub fn map<F, T2>(self, mut f: F) -> Page<T2, C>
189    where
190        F: FnMut(T) -> T2,
191    {
192        let edges = self
193            .edges
194            .into_iter()
195            .map(|edge| Edge {
196                cursor: edge.cursor,
197                node: f(edge.node),
198            })
199            .collect();
200        Page {
201            has_next_page: self.has_next_page,
202            has_previous_page: self.has_previous_page,
203            edges,
204        }
205    }
206
207    /// Try to map the items in this page with the given fallible function
208    ///
209    /// # Parameters
210    ///
211    /// * `f`: The fallible function to map the items with
212    ///
213    /// # Errors
214    ///
215    /// Returns the first error encountered while mapping the items
216    pub fn try_map<F, E, T2>(self, mut f: F) -> Result<Page<T2, C>, E>
217    where
218        F: FnMut(T) -> Result<T2, E>,
219    {
220        let edges: Result<Vec<Edge<T2, C>>, E> = self
221            .edges
222            .into_iter()
223            .map(|edge| {
224                Ok(Edge {
225                    cursor: edge.cursor,
226                    node: f(edge.node)?,
227                })
228            })
229            .collect();
230
231        Ok(Page {
232            has_next_page: self.has_next_page,
233            has_previous_page: self.has_previous_page,
234            edges: edges?,
235        })
236    }
237}