diff --git a/src/types/connection/mod.rs b/src/types/connection/mod.rs index ce7dd2f7..7dad25e2 100644 --- a/src/types/connection/mod.rs +++ b/src/types/connection/mod.rs @@ -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, + before: Option, + first: Option, + last: Option, +} + /// 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> { +/// async fn query_operation(&self, operation: &QueryOperation) -> FieldResult> { /// 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::().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::().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, -/// before: Option, +/// after: Option, +/// before: Option, /// first: Option, /// last: Option /// ) -> FieldResult> { @@ -133,42 +216,122 @@ pub trait DataSource: Sync + Send { async fn query( &self, _ctx: &Context<'_>, - after: Option, - before: Option, + after: Option, + before: Option, first: Option, last: Option, ) -> FieldResult> { - 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>; } diff --git a/src/types/connection/slice.rs b/src/types/connection/slice.rs index 4722a222..974bc706 100644 --- a/src/types/connection/slice.rs +++ b/src/types/connection/slice.rs @@ -9,36 +9,145 @@ impl<'a, T: Sync> DataSource for &'a [T] { async fn query_operation( &self, - operation: &QueryOperation<'_>, + operation: &QueryOperation, ) -> FieldResult> { 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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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))