diff --git a/CHANGELOG.md b/CHANGELOG.md index 243859f..5203fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Changed -- Updated the encoding and decoding method for GraphQL cursors by removing - `graphql::encode_cursor` and `graphql::decode_cursor` methods and replacing - them with the encoding and decoding methods of `OpaqueCursor`. +- The paginated GraphQL queries use different representations for cursors. The + cursor values obtained from earlier versions of the API are not compatible + with the new cursor values. ### Fixed diff --git a/Cargo.toml b/Cargo.toml index e8a4d4c..9c1f4ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ bincode = "1" chrono = { version = ">=0.4.35", default-features = false, features = [ "serde", ] } -data-encoding = "2" futures = "0.3" futures-util = "0.3" http = "1" diff --git a/src/graphql.rs b/src/graphql.rs index f8ed6cc..65cd7b8 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -270,8 +270,6 @@ where #[derive(Debug, thiserror::Error)] enum Error { - #[error("Invalid cursor")] - InvalidCursor, #[error("The value of first and last must be within 0-100")] InvalidLimitValue, #[error("You must provide a `first` or `last` value to properly paginate a connection.")] diff --git a/src/graphql/cluster.rs b/src/graphql/cluster.rs index cdf89f6..4b9133c 100644 --- a/src/graphql/cluster.rs +++ b/src/graphql/cluster.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use async_graphql::{ - connection::{Connection, Edge, EmptyFields}, + connection::{Connection, Edge, EmptyFields, OpaqueCursor}, types::ID, ComplexObject, Context, Object, Result, SimpleObject, StringNumber, }; @@ -17,7 +17,7 @@ use super::{ get_trend, model::{ModelDigest, TopElementCountsByColumn}, qualifier::Qualifier, - slicing::{self, IndexedKey}, + slicing, status::Status, Role, RoleGuard, DEFAULT_CUTOFF_RATE, DEFAULT_TRENDI_ORDER, }; @@ -46,7 +46,7 @@ impl ClusterQuery { before: Option<String>, first: Option<i32>, last: Option<i32>, - ) -> Result<Connection<IndexedKey<i64>, Cluster, ClusterTotalCount, EmptyFields>> { + ) -> Result<Connection<OpaqueCursor<(i32, i64)>, Cluster, ClusterTotalCount, EmptyFields>> { let model = model.as_str().parse()?; let categories = try_id_args_into_ints(categories)?; let detectors = try_id_args_into_ints(detectors)?; @@ -357,11 +357,11 @@ async fn load( detectors: Option<Vec<i32>>, qualifiers: Option<Vec<i32>>, statuses: Option<Vec<i32>>, - after: Option<IndexedKey<i64>>, - before: Option<IndexedKey<i64>>, + after: Option<OpaqueCursor<(i32, i64)>>, + before: Option<OpaqueCursor<(i32, i64)>>, first: Option<usize>, last: Option<usize>, -) -> Result<Connection<IndexedKey<i64>, Cluster, ClusterTotalCount, EmptyFields>> { +) -> Result<Connection<OpaqueCursor<(i32, i64)>, Cluster, ClusterTotalCount, EmptyFields>> { let is_first = first.is_some(); let limit = slicing::len(first, last)?; let db = ctx.data::<Database>()?; @@ -372,8 +372,8 @@ async fn load( detectors.as_deref(), qualifiers.as_deref(), statuses.as_deref(), - &after.map(Into::into), - &before.map(Into::into), + &after.map(|c| c.0), + &before.map(|c| c.0), is_first, limit, ) @@ -393,7 +393,7 @@ async fn load( ); connection.edges.extend(rows.into_iter().map(|c| { Edge::new( - IndexedKey::new(c.id, c.size), + OpaqueCursor((c.id, c.size)), Cluster { id: c.id, name: c.cluster_id, diff --git a/src/graphql/model.rs b/src/graphql/model.rs index e26fcc3..d1abb58 100644 --- a/src/graphql/model.rs +++ b/src/graphql/model.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use async_graphql::{ - connection::{Connection, Edge, EmptyFields}, + connection::{Connection, Edge, EmptyFields, OpaqueCursor}, types::ID, Context, Object, Result, SimpleObject, StringNumber, }; @@ -12,11 +12,8 @@ use review_database::{self as database, Database}; use tokio::sync::RwLock; use super::{ - cluster::TimeCount, - data_source::DataSource, - fill_vacant_time_slots, get_trend, - slicing::{self, IndexedKey}, - Role, RoleGuard, DEFAULT_CUTOFF_RATE, DEFAULT_TRENDI_ORDER, + cluster::TimeCount, data_source::DataSource, fill_vacant_time_slots, get_trend, slicing, Role, + RoleGuard, DEFAULT_CUTOFF_RATE, DEFAULT_TRENDI_ORDER, }; use crate::graphql::query; @@ -40,7 +37,8 @@ impl ModelQuery { before: Option<String>, first: Option<i32>, last: Option<i32>, - ) -> Result<Connection<IndexedKey<String>, ModelDigest, ModelTotalCount, EmptyFields>> { + ) -> Result<Connection<OpaqueCursor<(i32, String)>, ModelDigest, ModelTotalCount, EmptyFields>> + { query( after, before, @@ -902,21 +900,16 @@ impl ModelTotalCount { async fn load( ctx: &Context<'_>, - after: Option<IndexedKey<String>>, - before: Option<IndexedKey<String>>, + after: Option<OpaqueCursor<(i32, String)>>, + before: Option<OpaqueCursor<(i32, String)>>, first: Option<usize>, last: Option<usize>, -) -> Result<Connection<IndexedKey<String>, ModelDigest, ModelTotalCount, EmptyFields>> { +) -> Result<Connection<OpaqueCursor<(i32, String)>, ModelDigest, ModelTotalCount, EmptyFields>> { let is_first = first.is_some(); let limit = slicing::len(first, last)?; let db = ctx.data::<Database>()?; let rows = db - .load_models( - &after.map(Into::into), - &before.map(Into::into), - is_first, - limit, - ) + .load_models(&after.map(|c| c.0), &before.map(|c| c.0), is_first, limit) .await?; let (rows, has_previous, has_next) = slicing::page_info(is_first, limit, rows); @@ -924,7 +917,7 @@ async fn load( Connection::with_additional_fields(has_previous, has_next, ModelTotalCount); connection.edges.extend( rows.into_iter() - .map(|model| Edge::new(IndexedKey::new(model.id, model.name.clone()), model.into())), + .map(|model| Edge::new(OpaqueCursor((model.id, model.name.clone())), model.into())), ); Ok(connection) } diff --git a/src/graphql/slicing.rs b/src/graphql/slicing.rs index 785382f..a15aa15 100644 --- a/src/graphql/slicing.rs +++ b/src/graphql/slicing.rs @@ -1,48 +1,5 @@ -use std::{fmt, str::FromStr}; - -use async_graphql::connection::CursorType; -use data_encoding::BASE64; - use super::Error; -pub(super) struct IndexedKey<T> { - pub(crate) id: i32, - pub(crate) value: T, -} - -impl<T> IndexedKey<T> { - pub fn new(id: i32, value: T) -> Self { - Self { id, value } - } -} - -impl<T> From<IndexedKey<T>> for (i32, T) { - fn from(key: IndexedKey<T>) -> Self { - (key.id, key.value) - } -} - -impl<T: FromStr + fmt::Display> CursorType for IndexedKey<T> { - type Error = super::Error; - - fn decode_cursor(s: &str) -> Result<Self, Self::Error> { - let decoded = String::from_utf8( - BASE64 - .decode(s.as_bytes()) - .map_err(|_| Error::InvalidCursor)?, - ) - .map_err(|_| Error::InvalidCursor)?; - let (id, value) = decoded.split_once(':').ok_or(Error::InvalidCursor)?; - let id = id.parse().map_err(|_| Error::InvalidCursor)?; - let value = value.parse::<T>().map_err(|_| Error::InvalidCursor)?; - Ok(Self { id, value }) - } - - fn encode_cursor(&self) -> String { - BASE64.encode(format!("{}:{}", self.id, self.value).as_bytes()) - } -} - // This internal method should be called after validating pagination paramerters by either // `grapqhl::query` or `grapqhl::query_with_constraints`. pub(crate) fn len(first: Option<usize>, last: Option<usize>) -> Result<usize, Error> { diff --git a/src/graphql/statistics.rs b/src/graphql/statistics.rs index d2c099e..84b4228 100644 --- a/src/graphql/statistics.rs +++ b/src/graphql/statistics.rs @@ -9,10 +9,7 @@ use num_traits::ToPrimitive; use review_database::{BatchInfo, Database}; use serde_json::Value as JsonValue; -use super::{ - slicing::{self, IndexedKey}, - Role, RoleGuard, -}; +use super::{slicing, Role, RoleGuard}; use crate::graphql::{query, query_with_constraints}; #[derive(Default)] @@ -50,7 +47,7 @@ impl StatisticsQuery { last: Option<i32>, ) -> Result< Connection< - IndexedKey<i64>, + OpaqueCursor<(i32, i64)>, Round, TotalCountByCluster, EmptyFields, @@ -172,13 +169,13 @@ impl EdgeNameType for RoundByClusterEdge { async fn load_rounds_by_cluster( ctx: &Context<'_>, cluster: i32, - after: Option<IndexedKey<i64>>, - before: Option<IndexedKey<i64>>, + after: Option<OpaqueCursor<(i32, i64)>>, + before: Option<OpaqueCursor<(i32, i64)>>, first: Option<usize>, last: Option<usize>, ) -> Result< Connection< - IndexedKey<i64>, + OpaqueCursor<(i32, i64)>, Round, TotalCountByCluster, EmptyFields, @@ -192,8 +189,8 @@ async fn load_rounds_by_cluster( let (model, batches) = db .load_rounds_by_cluster( cluster, - &after.map(|k| i64_to_naive_date_time(k.value)), - &before.map(|k| i64_to_naive_date_time(k.value)), + &after.map(|k| i64_to_naive_date_time(k.0 .1)), + &before.map(|k| i64_to_naive_date_time(k.0 .1)), is_first, limit + 1, ) @@ -222,7 +219,7 @@ async fn load_rounds_by_cluster( connection.edges.extend( batch_infos .into_iter() - .map(|row| Edge::new(IndexedKey::new(cluster, row.inner.id), row.into())), + .map(|row| Edge::new(OpaqueCursor((cluster, row.inner.id)), row.into())), ); Ok(connection) }