Relay pagination algorithm
This commit is contained in:
parent
43cf45317d
commit
e696061666
|
@ -10,29 +10,98 @@ pub use connection_type::Connection;
|
|||
pub use cursor::Cursor;
|
||||
|
||||
/// Connection query operation
|
||||
pub enum QueryOperation<'a> {
|
||||
/// Forward query
|
||||
Forward {
|
||||
pub enum QueryOperation {
|
||||
/// Return all results
|
||||
None,
|
||||
/// Return all results after the cursor
|
||||
After {
|
||||
/// After this cursor
|
||||
after: Option<&'a str>,
|
||||
|
||||
/// How many records did this query return
|
||||
limit: usize,
|
||||
after: Cursor,
|
||||
},
|
||||
/// Backward query
|
||||
Backward {
|
||||
/// Return all results before the cursor
|
||||
Before {
|
||||
/// Before this cursor
|
||||
before: Option<&'a str>,
|
||||
|
||||
/// How many records did this query return
|
||||
before: Cursor,
|
||||
},
|
||||
/// Return all results between the cursors
|
||||
Between {
|
||||
/// After this cursor
|
||||
after: Cursor,
|
||||
/// But before this cursor
|
||||
before: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, starting from the beginning
|
||||
First {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, starting after the cursor
|
||||
FirstAfter {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// After this cursor
|
||||
after: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, starting from the beginning but ending before the cursor
|
||||
FirstBefore {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// Before this cursor
|
||||
before: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, but between the cursors. Limit includes beginning results.
|
||||
FirstBetween {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// After this cursor
|
||||
after: Cursor,
|
||||
/// But before this cursor
|
||||
before: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, but before the end
|
||||
Last {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, but before the end. Must not include anything before the cursor.
|
||||
LastAfter {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// After this cursor
|
||||
after: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, but before the cursor
|
||||
LastBefore {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// Before this cursor
|
||||
before: Cursor,
|
||||
},
|
||||
/// Return the amount of results specified by `limit`, but between the cursors. Limit includes ending results.
|
||||
LastBetween {
|
||||
/// The maximum amount of results to return
|
||||
limit: usize,
|
||||
/// After this cursor
|
||||
after: Cursor,
|
||||
/// But before this cursor
|
||||
before: Cursor,
|
||||
},
|
||||
/// An invalid query was made. For example: sending `first` and `last` in the same query
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Empty edge extension object
|
||||
#[async_graphql_derive::SimpleObject(internal)]
|
||||
pub struct EmptyEdgeFields;
|
||||
|
||||
// Temporary struct for to store values for pattern matching
|
||||
struct Pagination {
|
||||
after: Option<Cursor>,
|
||||
before: Option<Cursor>,
|
||||
first: Option<i32>,
|
||||
last: Option<i32>,
|
||||
}
|
||||
|
||||
/// Data source of GraphQL Cursor Connections type
|
||||
///
|
||||
/// `Edge` is an extension object type that extends the edge fields, If you don't need it, you can use `EmptyEdgeFields`.
|
||||
|
@ -60,23 +129,37 @@ pub struct EmptyEdgeFields;
|
|||
/// type Element = i32;
|
||||
/// type EdgeFieldsObj = DiffFields;
|
||||
///
|
||||
/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
|
||||
/// async fn query_operation(&self, operation: &QueryOperation) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
|
||||
/// let (start, end) = match operation {
|
||||
/// QueryOperation::Forward {after, limit} => {
|
||||
/// let start = after.and_then(|after| base64::decode(after).ok())
|
||||
/// QueryOperation::First {limit} => {
|
||||
/// let start = 0;
|
||||
/// let end = start + *limit as i32;
|
||||
/// (start, end)
|
||||
/// }
|
||||
/// QueryOperation::Last {limit} => {
|
||||
/// let end = 0;
|
||||
/// let start = end - *limit as i32;
|
||||
/// (start, end)
|
||||
/// }
|
||||
/// QueryOperation::FirstAfter {after, limit} => {
|
||||
/// let start = base64::decode(after.to_string())
|
||||
/// .ok()
|
||||
/// .and_then(|data| data.as_slice().read_i32::<BE>().ok())
|
||||
/// .map(|idx| idx + 1)
|
||||
/// .unwrap_or(0);
|
||||
/// let end = start + *limit as i32;
|
||||
/// (start, end)
|
||||
/// }
|
||||
/// QueryOperation::Backward {before, limit} => {
|
||||
/// let end = before.and_then(|before| base64::decode(before).ok())
|
||||
/// QueryOperation::LastBefore {before, limit} => {
|
||||
/// let end = base64::decode(before.to_string())
|
||||
/// .ok()
|
||||
/// .and_then(|data| data.as_slice().read_i32::<BE>().ok())
|
||||
/// .unwrap_or(0);
|
||||
/// let start = end - *limit as i32;
|
||||
/// (start, end)
|
||||
/// }
|
||||
/// // You should handle all cases instead of using a default like this
|
||||
/// _ => (0, 10)
|
||||
/// };
|
||||
///
|
||||
/// let nodes = (start..end).into_iter().map(|n| (base64::encode(n.to_be_bytes()).into(), DiffFields {diff: n - 1000}, n)).collect();
|
||||
|
@ -87,8 +170,8 @@ pub struct EmptyEdgeFields;
|
|||
/// #[Object]
|
||||
/// impl QueryRoot {
|
||||
/// async fn numbers(&self, ctx: &Context<'_>,
|
||||
/// after: Option<String>,
|
||||
/// before: Option<String>,
|
||||
/// after: Option<Cursor>,
|
||||
/// before: Option<Cursor>,
|
||||
/// first: Option<i32>,
|
||||
/// last: Option<i32>
|
||||
/// ) -> FieldResult<Connection<i32, DiffFields>> {
|
||||
|
@ -133,42 +216,122 @@ pub trait DataSource: Sync + Send {
|
|||
async fn query(
|
||||
&self,
|
||||
_ctx: &Context<'_>,
|
||||
after: Option<String>,
|
||||
before: Option<String>,
|
||||
after: Option<Cursor>,
|
||||
before: Option<Cursor>,
|
||||
first: Option<i32>,
|
||||
last: Option<i32>,
|
||||
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
|
||||
let operation = if let Some(after) = &after {
|
||||
QueryOperation::Forward {
|
||||
after: Some(after),
|
||||
limit: match first {
|
||||
Some(value) => value.max(0) as usize,
|
||||
None => 10,
|
||||
},
|
||||
}
|
||||
} else if let Some(before) = &before {
|
||||
QueryOperation::Backward {
|
||||
before: Some(before),
|
||||
limit: match last {
|
||||
Some(value) => value.max(0) as usize,
|
||||
None => 10,
|
||||
},
|
||||
}
|
||||
} else if let Some(first) = first {
|
||||
QueryOperation::Forward {
|
||||
after: None,
|
||||
limit: first.max(0) as usize,
|
||||
}
|
||||
} else if let Some(last) = last {
|
||||
QueryOperation::Backward {
|
||||
let pagination = Pagination {
|
||||
first,
|
||||
last,
|
||||
before,
|
||||
after,
|
||||
};
|
||||
|
||||
let operation = match pagination {
|
||||
// This is technically allowed according to the Relay Spec, but highly discouraged
|
||||
Pagination {
|
||||
first: Some(_),
|
||||
last: Some(_),
|
||||
before: _,
|
||||
after: _,
|
||||
} => QueryOperation::Invalid,
|
||||
Pagination {
|
||||
first: None,
|
||||
last: None,
|
||||
before: None,
|
||||
limit: last.max(0) as usize,
|
||||
}
|
||||
} else {
|
||||
QueryOperation::Forward {
|
||||
after: None,
|
||||
limit: 10,
|
||||
}
|
||||
} => QueryOperation::None,
|
||||
Pagination {
|
||||
first: None,
|
||||
last: None,
|
||||
before: Some(before),
|
||||
after: None,
|
||||
} => QueryOperation::Before { before },
|
||||
Pagination {
|
||||
first: None,
|
||||
last: None,
|
||||
before: None,
|
||||
after: Some(after),
|
||||
} => QueryOperation::After { after },
|
||||
Pagination {
|
||||
first: None,
|
||||
last: None,
|
||||
before: Some(before),
|
||||
after: Some(after),
|
||||
} => QueryOperation::Between { after, before },
|
||||
Pagination {
|
||||
first: Some(limit),
|
||||
last: None,
|
||||
before: None,
|
||||
after: None,
|
||||
} => QueryOperation::First {
|
||||
limit: limit.max(0) as usize,
|
||||
},
|
||||
Pagination {
|
||||
first: Some(limit),
|
||||
last: None,
|
||||
before: Some(before),
|
||||
after: None,
|
||||
} => QueryOperation::FirstBefore {
|
||||
limit: limit.max(0) as usize,
|
||||
before,
|
||||
},
|
||||
Pagination {
|
||||
first: Some(limit),
|
||||
last: None,
|
||||
before: None,
|
||||
after: Some(after),
|
||||
} => QueryOperation::FirstAfter {
|
||||
limit: limit.max(0) as usize,
|
||||
after,
|
||||
},
|
||||
Pagination {
|
||||
first: Some(limit),
|
||||
last: None,
|
||||
before: Some(before),
|
||||
after: Some(after),
|
||||
} => QueryOperation::FirstBetween {
|
||||
limit: limit.max(0) as usize,
|
||||
after,
|
||||
before,
|
||||
},
|
||||
Pagination {
|
||||
first: None,
|
||||
last: Some(limit),
|
||||
before: None,
|
||||
after: None,
|
||||
} => QueryOperation::Last {
|
||||
limit: limit.max(0) as usize,
|
||||
},
|
||||
Pagination {
|
||||
first: None,
|
||||
last: Some(limit),
|
||||
before: Some(before),
|
||||
after: None,
|
||||
} => QueryOperation::LastBefore {
|
||||
limit: limit.max(0) as usize,
|
||||
before,
|
||||
},
|
||||
Pagination {
|
||||
first: None,
|
||||
last: Some(limit),
|
||||
before: None,
|
||||
after: Some(after),
|
||||
} => QueryOperation::LastAfter {
|
||||
limit: limit.max(0) as usize,
|
||||
after,
|
||||
},
|
||||
Pagination {
|
||||
first: None,
|
||||
last: Some(limit),
|
||||
before: Some(before),
|
||||
after: Some(after),
|
||||
} => QueryOperation::LastBetween {
|
||||
limit: limit.max(0) as usize,
|
||||
after,
|
||||
before,
|
||||
},
|
||||
};
|
||||
|
||||
self.query_operation(&operation).await
|
||||
|
@ -177,6 +340,6 @@ pub trait DataSource: Sync + Send {
|
|||
/// Parses the parameters and executes the query,Usually you just need to implement this method.
|
||||
async fn query_operation(
|
||||
&self,
|
||||
operation: &QueryOperation<'_>,
|
||||
operation: &QueryOperation,
|
||||
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>>;
|
||||
}
|
||||
|
|
|
@ -9,36 +9,145 @@ impl<'a, T: Sync> DataSource for &'a [T] {
|
|||
|
||||
async fn query_operation(
|
||||
&self,
|
||||
operation: &QueryOperation<'_>,
|
||||
operation: &QueryOperation,
|
||||
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
|
||||
let (start, end) = match operation {
|
||||
QueryOperation::Forward { after, limit } => {
|
||||
let start = after
|
||||
.and_then(|after| base64::decode(after).ok())
|
||||
QueryOperation::None => {
|
||||
let start = 0;
|
||||
let end = self.len();
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::After { after } => {
|
||||
let start = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let end = self.len();
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::Before { before } => {
|
||||
let end = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
let start = 0;
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::Between { after, before } => {
|
||||
let start = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let end = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::First { limit } => {
|
||||
let start = 0;
|
||||
let end = (start + *limit).min(self.len());
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::FirstAfter { after, limit } => {
|
||||
let start = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let end = (start + *limit).min(self.len());
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::Backward { before, limit } => {
|
||||
let end = before
|
||||
.and_then(|before| base64::decode(before).ok())
|
||||
QueryOperation::FirstBefore { before, limit } => {
|
||||
let end_cursor = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
let start = if end < *limit { 0 } else { end - *limit };
|
||||
let start = (end_cursor - *limit).max(0);
|
||||
let end = (start + *limit).min(end_cursor);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::FirstBetween {
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
} => {
|
||||
let start = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let end_cursor = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
let end = (start + *limit).min(end_cursor);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::Last { limit } => {
|
||||
let end = self.len();
|
||||
let start = (end - *limit).max(0);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::LastAfter { after, limit } => {
|
||||
let end = self.len();
|
||||
let start_cursor = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let start = (end - *limit).max(start_cursor);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::LastBefore { before, limit } => {
|
||||
let end = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
let start = (end - *limit).max(0);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::LastBetween {
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
} => {
|
||||
let start_cursor = base64::decode(after.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| (idx + 1) as usize)
|
||||
.unwrap_or(0);
|
||||
let end = base64::decode(before.to_string())
|
||||
.ok()
|
||||
.and_then(|data| data.as_slice().read_u32::<BE>().ok())
|
||||
.map(|idx| idx as usize)
|
||||
.unwrap_or_else(|| self.len());
|
||||
let start = (end - *limit).max(start_cursor);
|
||||
(start, end)
|
||||
}
|
||||
QueryOperation::Invalid => {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
(start, end)
|
||||
}
|
||||
};
|
||||
|
||||
let mut nodes = Vec::with_capacity(end - start);
|
||||
for (idx, item) in self[start..end].iter().enumerate() {
|
||||
nodes.push((
|
||||
base64::encode((idx as u32).to_be_bytes()).into(),
|
||||
EmptyEdgeFields,
|
||||
item,
|
||||
));
|
||||
if nodes.capacity() != 0 {
|
||||
for (idx, item) in self[start..end].iter().enumerate() {
|
||||
nodes.push((
|
||||
base64::encode((idx as u32).to_be_bytes()).into(),
|
||||
EmptyEdgeFields,
|
||||
item,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Connection::new(None, start > 0, end < self.len(), nodes))
|
||||
|
|
Loading…
Reference in New Issue
Block a user