Skip to main content

es_entity/
pagination.rs

1//! Control and customize the query execution and its response.
2
3/// Controls the sorting order when listing the entities from the database
4///
5/// `ListDirection` enum is used to specify order when listing entities from the database using [`EsRepo`][crate::EsRepo]
6/// generated functions like `list_by`, `list_for` and `list_for_filters`. Has two variants: `Ascending` and `Descending`
7///
8/// # Examples
9///
10/// ```ignore
11/// // List users by ID in ascending order (oldest first)
12/// let paginated_users_asc = users.list_by_id(
13///     PaginatedQueryArgs { first: 5, after: None },
14///     ListDirection::Ascending, // or just Default::default()
15/// ).await?
16///
17/// // List users by name in descending order (Z to A)
18/// let paginated_users_desc = users.list_by_name(
19///     PaginatedQueryArgs { first: 5, after: None },
20///     ListDirection::Descending,
21/// ).await?
22/// ```
23#[derive(Default, std::fmt::Debug, Clone, Copy)]
24pub enum ListDirection {
25    /// Sets the default direction variant to `Ascending`
26    #[default]
27    Ascending,
28    Descending,
29}
30
31/// Structure to sort entities on a specific field when listing from database
32///
33/// Sort enum is used to specify the sorting order and the field to sort the entities by when listing them using `list_for_filters`
34/// generated by [`EsRepo`][crate::EsRepo]. It is generated automatically for all fields which have `list_by` option. It encapsulates two fields,
35/// first is `by` to specify the field name, second is `direction` which takes [`ListDirection`]
36///
37/// # Example
38///
39/// ```ignore
40/// let result = users.list_for_filters(
41///     UserFilters {
42///         name: Some("Murphy".to_string()),
43///     },
44///     Sort {
45///         // `UserSortBy::Id` and `UserSortBy::CreatedAt` are created by default,
46///         // other columns need `list_by` to sort by
47///         by: UserSortBy::Id,
48///         direction: ListDirection::Descending,
49///     },
50///     PaginatedQueryArgs {
51///         first: 10,
52///         after: Default::default(),
53///     },
54/// )
55/// .await?;
56/// ```
57#[derive(std::fmt::Debug, Clone, Copy)]
58pub struct Sort<T> {
59    // T parameter represents the field
60    pub by: T,
61    pub direction: ListDirection,
62}
63
64/// A cursor-based pagination structure for efficiently paginating through large datasets
65///
66/// The `PaginatedQueryArgs<T>` encapsulates a `first` field that specifies the count of entities to fetch per query, and an optional `after` field
67/// that specifies the cursor to start from for the current query. The `<T>` parameter represents cursor type, which depends on the sorting field.
68/// A field's cursor is generated by [`EsRepo`][crate::EsRepo] if it has `list_by` option.
69///
70/// # Examples
71///
72/// ```ignore
73/// // Initial query - fetch first 10 users after an existing user id
74/// let query_args = PaginatedQueryArgs {
75///     first: 10,
76///     after: Some(user_cursor::UserByIdCursor {
77///         id: some_existing_user_id // assume this variable exists
78///     }),
79/// };
80///
81/// // Execute query using `query_args` argument of `PaginatedQueryArgs` type
82/// let result = users.list_by_id(query_args, ListDirection::Ascending).await?;
83///
84/// // Continue pagination using the updated `next_query_args` of `PaginatedQueryArgs` type
85/// if result.has_next_page {
86///     let next_query_args = PaginatedQueryArgs {
87///         first: 10,
88///         after: result.end_cursor, // Use cursor from previous result to update `after`
89///     };
90///     let next_result = users.list_by_id(next_query_args, ListDirection::Ascending).await?;
91/// }
92/// ```
93#[derive(Debug)]
94pub struct PaginatedQueryArgs<T: std::fmt::Debug> {
95    /// Specifies the number of entities to fetch per query
96    pub first: usize,
97    /// Specifies the cursor/marker to start from for current query
98    pub after: Option<T>,
99}
100
101impl<T: std::fmt::Debug> Clone for PaginatedQueryArgs<T>
102where
103    T: Clone,
104{
105    fn clone(&self) -> Self {
106        Self {
107            first: self.first,
108            after: self.after.clone(),
109        }
110    }
111}
112
113impl<T: std::fmt::Debug> Default for PaginatedQueryArgs<T> {
114    /// Default value fetches first 100 entities
115    fn default() -> Self {
116        Self {
117            first: 100,
118            after: None,
119        }
120    }
121}
122
123/// Return type for paginated queries containing entities and pagination metadata
124///
125/// `PaginatedQueryRet` contains the fetched entities and utilities for continuing pagination.
126/// Returned by the [`EsRepo`][crate::EsRepo] functions like `list_by`, `list_for` and `list_for_filters`.
127/// Used with [`PaginatedQueryArgs`] to perform consistent and efficient pagination
128///
129/// # Examples
130///
131/// ```ignore
132/// let mut query = PaginatedQueryArgs { first: 10, after: None };
133/// let mut all_users = Vec::new();
134///
135/// loop {
136///     let result = users.list_by_id(query, ListDirection::Ascending).await?;
137///     let (chunk, next) = result.into_parts();
138///     all_users.extend(chunk);
139///
140///     match next {
141///         Some(next_query) => query = next_query,
142///         None => break,
143///     }
144/// }
145/// ```
146pub struct PaginatedQueryRet<T, C> {
147    requested_size: usize,
148    entities: Vec<T>,
149    /// [bool] for indicating if the list has been exhausted or more entities can be fetched
150    pub has_next_page: bool,
151    /// cursor on the last entity fetched to continue paginated queries.
152    pub end_cursor: Option<C>,
153}
154
155impl<T, C> PaginatedQueryRet<T, C> {
156    /// `requested_size` must be the `first` that produced this page, not `entities.len()`.
157    pub fn new(
158        entities: Vec<T>,
159        has_next_page: bool,
160        end_cursor: Option<C>,
161        requested_size: usize,
162    ) -> Self {
163        Self {
164            requested_size,
165            entities,
166            has_next_page,
167            end_cursor,
168        }
169    }
170
171    pub fn requested_size(&self) -> usize {
172        self.requested_size
173    }
174
175    pub fn entities(&self) -> &[T] {
176        &self.entities
177    }
178
179    pub fn into_parts(self) -> (Vec<T>, Option<PaginatedQueryArgs<C>>)
180    where
181        C: std::fmt::Debug,
182    {
183        let continuation = if self.has_next_page && self.requested_size > 0 {
184            Some(PaginatedQueryArgs {
185                first: self.requested_size,
186                after: self.end_cursor,
187            })
188        } else {
189            None
190        };
191        (self.entities, continuation)
192    }
193
194    pub fn into_next_query(self) -> Option<PaginatedQueryArgs<C>>
195    where
196        C: std::fmt::Debug,
197    {
198        self.into_parts().1
199    }
200
201    pub fn map_end_cursor<C2>(self, f: impl FnOnce(C) -> C2) -> PaginatedQueryRet<T, C2> {
202        PaginatedQueryRet {
203            requested_size: self.requested_size,
204            entities: self.entities,
205            has_next_page: self.has_next_page,
206            end_cursor: self.end_cursor.map(f),
207        }
208    }
209
210    pub fn map_entities<T2>(self, f: impl FnMut(T) -> T2) -> PaginatedQueryRet<T2, C> {
211        PaginatedQueryRet {
212            requested_size: self.requested_size,
213            entities: self.entities.into_iter().map(f).collect(),
214            has_next_page: self.has_next_page,
215            end_cursor: self.end_cursor,
216        }
217    }
218
219    pub fn try_map_entities<T2, E>(
220        self,
221        f: impl FnMut(T) -> Result<T2, E>,
222    ) -> Result<PaginatedQueryRet<T2, C>, E> {
223        Ok(PaginatedQueryRet {
224            requested_size: self.requested_size,
225            entities: self.entities.into_iter().map(f).collect::<Result<_, E>>()?,
226            has_next_page: self.has_next_page,
227            end_cursor: self.end_cursor,
228        })
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn next_query_preserves_page_size_after_entities_are_moved() {
238        for page_size in [1, 3, 100] {
239            let page = PaginatedQueryRet::new(
240                (0..page_size).collect(),
241                true,
242                Some(page_size - 1),
243                page_size,
244            );
245            let (collected, next) = page.into_parts();
246            let next = next.expect("another page exists");
247
248            assert_eq!(next.first, page_size);
249            assert_eq!(next.after, Some(page_size - 1));
250            assert_eq!(collected.len(), page_size);
251        }
252    }
253
254    #[test]
255    fn next_query_size_is_independent_of_remaining_entities() {
256        for count in [0, 1, 3, 7, 14] {
257            let page =
258                PaginatedQueryRet::new(vec![(); count], true, Some("last-fetched-entity"), 7);
259
260            let next = page.into_next_query().expect("another page exists");
261            assert_eq!(next.first, 7);
262            assert_eq!(next.after, Some("last-fetched-entity"));
263        }
264    }
265
266    #[test]
267    fn last_page_has_no_next_query() {
268        for count in [0, 1, 7] {
269            let page = PaginatedQueryRet::new(vec![(); count], false, count.checked_sub(1), 7);
270
271            assert!(page.into_next_query().is_none());
272        }
273    }
274
275    #[test]
276    fn zero_page_size_does_not_continue_even_when_more_entities_exist() {
277        let page = PaginatedQueryRet::<(), usize>::new(Vec::new(), true, None, 0);
278
279        assert!(page.into_next_query().is_none());
280    }
281
282    #[test]
283    fn into_parts_returns_entities_together_with_the_next_query() {
284        let page = PaginatedQueryRet::new(vec![(); 7], true, Some(6), 10);
285        assert_eq!(page.requested_size(), 10);
286
287        let (entities, next) = page.into_parts();
288        assert_eq!(entities.len(), 7);
289        let next = next.expect("another page exists");
290        assert_eq!(next.first, 10);
291        assert_eq!(next.after, Some(6));
292    }
293
294    #[test]
295    fn mapping_entities_keeps_requested_size_and_cursor_on_a_short_last_page() {
296        let page = PaginatedQueryRet::new(vec![1u32, 2, 3], false, Some("last"), 10);
297
298        let mapped = page.map_entities(|n| n.to_string());
299
300        assert_eq!(mapped.entities(), ["1", "2", "3"]);
301        assert_eq!(
302            mapped.requested_size(),
303            10,
304            "requested size must survive an entity type change"
305        );
306        assert_eq!(
307            mapped.end_cursor,
308            Some("last"),
309            "end_cursor must survive on a page with no continuation"
310        );
311    }
312
313    #[test]
314    fn try_mapping_entities_keeps_metadata_and_propagates_the_first_failure() {
315        let page = PaginatedQueryRet::new(vec![1u32, 2], true, Some("last"), 10);
316        let mapped = page
317            .try_map_entities(|n| u8::try_from(n).map_err(|_| "unconvertible"))
318            .expect("all entities convert");
319        assert_eq!(mapped.entities(), [1u8, 2]);
320        assert_eq!(mapped.requested_size(), 10);
321        assert_eq!(mapped.end_cursor, Some("last"));
322
323        let page = PaginatedQueryRet::new(vec![1u32, u32::MAX], true, Some("last"), 10);
324        let err = page
325            .try_map_entities(|n| u8::try_from(n).map_err(|_| "unconvertible"))
326            .err()
327            .expect("the out-of-range entity must abort the conversion");
328        assert_eq!(err, "unconvertible");
329    }
330}