|
| 1 | +use crate::models::helpers::with_count::*; |
| 2 | +use crate::util::errors::*; |
1 | 3 | use diesel::pg::Pg;
|
2 | 4 | use diesel::prelude::*;
|
3 | 5 | use diesel::query_builder::*;
|
| 6 | +use diesel::query_dsl::LoadQuery; |
4 | 7 | use diesel::sql_types::BigInt;
|
| 8 | +use indexmap::IndexMap; |
| 9 | + |
| 10 | +#[derive(Debug, Clone, Copy)] |
| 11 | +pub(crate) enum Page { |
| 12 | + Numeric(u32), |
| 13 | + Unspecified, |
| 14 | +} |
| 15 | + |
| 16 | +impl Page { |
| 17 | + fn new(params: &IndexMap<String, String>) -> CargoResult<Self> { |
| 18 | + if let Some(s) = params.get("page") { |
| 19 | + let numeric_page = s.parse()?; |
| 20 | + if numeric_page < 1 { |
| 21 | + return Err(human(&format_args!( |
| 22 | + "page indexing starts from 1, page {} is invalid", |
| 23 | + numeric_page, |
| 24 | + ))); |
| 25 | + } |
| 26 | + |
| 27 | + Ok(Page::Numeric(numeric_page)) |
| 28 | + } else { |
| 29 | + Ok(Page::Unspecified) |
| 30 | + } |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +#[derive(Debug, Clone, Copy)] |
| 35 | +pub(crate) struct PaginationOptions { |
| 36 | + page: Page, |
| 37 | + pub(crate) per_page: u32, |
| 38 | +} |
| 39 | + |
| 40 | +impl PaginationOptions { |
| 41 | + pub(crate) fn new(params: &IndexMap<String, String>) -> CargoResult<Self> { |
| 42 | + const DEFAULT_PER_PAGE: u32 = 10; |
| 43 | + const MAX_PER_PAGE: u32 = 100; |
| 44 | + |
| 45 | + let per_page = params |
| 46 | + .get("per_page") |
| 47 | + .map(|s| s.parse()) |
| 48 | + .unwrap_or(Ok(DEFAULT_PER_PAGE))?; |
| 49 | + |
| 50 | + if per_page > MAX_PER_PAGE { |
| 51 | + return Err(human(&format_args!( |
| 52 | + "cannot request more than {} items", |
| 53 | + MAX_PER_PAGE, |
| 54 | + ))); |
| 55 | + } |
| 56 | + |
| 57 | + Ok(Self { |
| 58 | + page: Page::new(params)?, |
| 59 | + per_page, |
| 60 | + }) |
| 61 | + } |
| 62 | + |
| 63 | + pub(crate) fn offset(&self) -> Option<u32> { |
| 64 | + if let Page::Numeric(p) = self.page { |
| 65 | + Some((p - 1) * self.per_page) |
| 66 | + } else { |
| 67 | + None |
| 68 | + } |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +pub(crate) trait Paginate: Sized { |
| 73 | + fn paginate(self, params: &IndexMap<String, String>) -> CargoResult<PaginatedQuery<Self>> { |
| 74 | + Ok(PaginatedQuery { |
| 75 | + query: self, |
| 76 | + options: PaginationOptions::new(params)?, |
| 77 | + }) |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +impl<T> Paginate for T {} |
5 | 82 |
|
6 |
| -#[derive(Debug, QueryId)] |
7 | 83 | pub struct Paginated<T> {
|
8 |
| - query: T, |
9 |
| - limit: i64, |
10 |
| - offset: i64, |
| 84 | + records_and_total: Vec<WithCount<T>>, |
| 85 | + options: PaginationOptions, |
11 | 86 | }
|
12 | 87 |
|
13 |
| -pub trait Paginate: AsQuery + Sized { |
14 |
| - fn paginate(self, limit: i64, offset: i64) -> Paginated<Self::Query> { |
15 |
| - Paginated { |
16 |
| - query: self.as_query(), |
17 |
| - limit, |
18 |
| - offset, |
| 88 | +impl<T> Paginated<T> { |
| 89 | + pub(crate) fn total(&self) -> Option<i64> { |
| 90 | + Some( |
| 91 | + self.records_and_total |
| 92 | + .get(0) |
| 93 | + .map(|row| row.total) |
| 94 | + .unwrap_or_default(), |
| 95 | + ) |
| 96 | + } |
| 97 | + |
| 98 | + pub(crate) fn next_page_params(&self) -> Option<IndexMap<String, String>> { |
| 99 | + if self.records_and_total.len() < self.options.per_page as usize { |
| 100 | + return None; |
19 | 101 | }
|
| 102 | + |
| 103 | + let mut opts = IndexMap::new(); |
| 104 | + match self.options.page { |
| 105 | + Page::Numeric(n) => opts.insert("page".into(), (n + 1).to_string()), |
| 106 | + Page::Unspecified => opts.insert("page".into(), 2.to_string()), |
| 107 | + }; |
| 108 | + Some(opts) |
| 109 | + } |
| 110 | + |
| 111 | + pub(crate) fn prev_page_params(&self) -> Option<IndexMap<String, String>> { |
| 112 | + if let Page::Numeric(1) | Page::Unspecified = self.options.page { |
| 113 | + return None; |
| 114 | + } |
| 115 | + |
| 116 | + let mut opts = IndexMap::new(); |
| 117 | + match self.options.page { |
| 118 | + Page::Numeric(n) => opts.insert("page".into(), (n - 1).to_string()), |
| 119 | + Page::Unspecified => unreachable!(), |
| 120 | + }; |
| 121 | + Some(opts) |
| 122 | + } |
| 123 | + |
| 124 | + pub(crate) fn iter(&self) -> impl Iterator<Item = &T> { |
| 125 | + self.records_and_total.iter().map(|row| &row.record) |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +impl<T: 'static> IntoIterator for Paginated<T> { |
| 130 | + type IntoIter = Box<dyn Iterator<Item = Self::Item>>; |
| 131 | + type Item = T; |
| 132 | + |
| 133 | + fn into_iter(self) -> Self::IntoIter { |
| 134 | + Box::new(self.records_and_total.into_iter().map(|row| row.record)) |
20 | 135 | }
|
21 | 136 | }
|
22 | 137 |
|
23 |
| -impl<T: AsQuery> Paginate for T {} |
| 138 | +#[derive(Debug)] |
| 139 | +pub(crate) struct PaginatedQuery<T> { |
| 140 | + query: T, |
| 141 | + options: PaginationOptions, |
| 142 | +} |
24 | 143 |
|
25 |
| -impl<T: Query> Query for Paginated<T> { |
| 144 | +impl<T> PaginatedQuery<T> { |
| 145 | + pub(crate) fn load<U>(self, conn: &PgConnection) -> QueryResult<Paginated<U>> |
| 146 | + where |
| 147 | + Self: LoadQuery<PgConnection, WithCount<U>>, |
| 148 | + { |
| 149 | + let options = self.options; |
| 150 | + let records_and_total = self.internal_load(conn)?; |
| 151 | + Ok(Paginated { |
| 152 | + records_and_total, |
| 153 | + options, |
| 154 | + }) |
| 155 | + } |
| 156 | +} |
| 157 | + |
| 158 | +impl<T> QueryId for PaginatedQuery<T> { |
| 159 | + const HAS_STATIC_QUERY_ID: bool = false; |
| 160 | + type QueryId = (); |
| 161 | +} |
| 162 | + |
| 163 | +impl<T: Query> Query for PaginatedQuery<T> { |
26 | 164 | type SqlType = (T::SqlType, BigInt);
|
27 | 165 | }
|
28 | 166 |
|
29 |
| -impl<T, DB> RunQueryDsl<DB> for Paginated<T> {} |
| 167 | +impl<T, DB> RunQueryDsl<DB> for PaginatedQuery<T> {} |
30 | 168 |
|
31 |
| -impl<T> QueryFragment<Pg> for Paginated<T> |
| 169 | +impl<T> QueryFragment<Pg> for PaginatedQuery<T> |
32 | 170 | where
|
33 | 171 | T: QueryFragment<Pg>,
|
34 | 172 | {
|
35 | 173 | fn walk_ast(&self, mut out: AstPass<'_, Pg>) -> QueryResult<()> {
|
36 | 174 | out.push_sql("SELECT *, COUNT(*) OVER () FROM (");
|
37 | 175 | self.query.walk_ast(out.reborrow())?;
|
38 | 176 | out.push_sql(") t LIMIT ");
|
39 |
| - out.push_bind_param::<BigInt, _>(&self.limit)?; |
40 |
| - out.push_sql(" OFFSET "); |
41 |
| - out.push_bind_param::<BigInt, _>(&self.offset)?; |
| 177 | + out.push_bind_param::<BigInt, _>(&i64::from(self.options.per_page))?; |
| 178 | + if let Some(offset) = self.options.offset() { |
| 179 | + out.push_sql(" OFFSET "); |
| 180 | + out.push_bind_param::<BigInt, _>(&i64::from(offset))?; |
| 181 | + } |
42 | 182 | Ok(())
|
43 | 183 | }
|
44 | 184 | }
|
0 commit comments