diff --git a/cli/src/torii/routing.rs b/cli/src/torii/routing.rs index 2e86f43de06..8c92afbae69 100644 --- a/cli/src/torii/routing.rs +++ b/cli/src/torii/routing.rs @@ -119,12 +119,29 @@ pub(crate) async fn handle_queries( wsv: Arc, query_judge: QueryJudgeArc, pagination: Pagination, + sorting: Sorting, request: VerifiedQueryRequest, ) -> Result> { let (valid_request, filter) = request.validate(&wsv, query_judge.as_ref())?; let original_result = valid_request.execute(&wsv)?; let result = filter.filter(original_result); - let (total, result) = if let Value::Vec(value) = result { + let (total, result) = if let Value::Vec(mut value) = result { + if let Some(ref key) = sorting.sort_by_key { + value.sort_by_key(|value0| match value0 { + Value::Identifiable(IdentifiableBox::Asset(asset)) => match asset.value() { + AssetValue::Store(metadata) => metadata.get(key).map_or(0, |value1| { + if let Value::U128(x) = value1 { + *x + } else { + 0 + } + }), + _ => 0, + }, + _ => 0, + }); + } + ( value.len(), Value::Vec(value.into_iter().paginate(pagination).collect()), @@ -482,11 +499,12 @@ impl Torii { )) .and(body::versioned()), ) - .or(endpoint4( + .or(endpoint5( handle_queries, warp::path(uri::QUERY) .and(add_state!(self.wsv, self.query_judge)) .and(paginate()) + .and(sorting()) .and(body::query()), )) .or(endpoint2( diff --git a/cli/src/torii/tests.rs b/cli/src/torii/tests.rs index 7b555ec1d27..f70c971f843 100644 --- a/cli/src/torii/tests.rs +++ b/cli/src/torii/tests.rs @@ -109,10 +109,12 @@ async fn torii_pagination() { .expect("Failed to verify"); let pagination = Pagination { start, limit }; + let sorting = Sorting::default(); handle_queries( Arc::clone(&torii.wsv), Arc::clone(&torii.query_judge), pagination, + sorting, query, ) .map(|result| { diff --git a/cli/src/torii/utils.rs b/cli/src/torii/utils.rs index f8072016fd5..5f9be2865a4 100644 --- a/cli/src/torii/utils.rs +++ b/cli/src/torii/utils.rs @@ -92,4 +92,4 @@ impl Reply for WarpResult { } } -generate_endpoints!(2, 3, 4); +generate_endpoints!(2, 3, 4, 5); diff --git a/client/src/client.rs b/client/src/client.rs index b34dff6226f..32e2268696e 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -611,6 +611,7 @@ impl Client { &self, request: R, pagination: Pagination, + sorting: Sorting, filter: PredicateBox, ) -> Result<(B, QueryResponseHandler)> where @@ -619,6 +620,7 @@ impl Client { B: RequestBuilder, { let pagination: Vec<_> = pagination.into(); + let sorting: Vec<_> = sorting.into(); let request = QueryRequest::new(request.into(), self.account_id.clone(), filter); let request: VersionedSignedQueryRequest = self.sign_query(request)?.into(); @@ -628,33 +630,80 @@ impl Client { format!("{}/{}", &self.torii_url, uri::QUERY), ) .params(pagination) + .params(sorting) .headers(self.headers.clone()) .body(request.encode_versioned()), QueryResponseHandler::default(), )) } - /// Create a request with pagination and add the filter. + /// Create a request with pagination, sorting and add the filter. /// /// # Errors - /// Forwards from [`Self::prepare_query_request`]. - pub fn request_with_pagination_and_filter( + /// Fails if sending request fails + pub fn request_with_pagination_and_filter_and_sorting( &self, request: R, pagination: Pagination, + sorting: Sorting, filter: PredicateBox, ) -> QueryHandlerResult> where R: Query + Into + Debug, >::Error: Into, // Seems redundant { - iroha_logger::trace!(?request, %pagination, ?filter); - let (req, resp_handler) = - self.prepare_query_request::(request, pagination, filter)?; + iroha_logger::trace!(?request, %pagination, ?sorting, ?filter); + let (req, resp_handler) = self.prepare_query_request::( + request, pagination, sorting, filter, + )?; let response = req.build()?.send()?; resp_handler.handle(response) } + /// Create a request with pagination and sorting. + /// + /// # Errors + /// Fails if sending request fails + pub fn request_with_pagination_and_sorting( + &self, + request: R, + pagination: Pagination, + sorting: Sorting, + ) -> QueryHandlerResult> + where + R: Query + Into + Debug, + >::Error: Into, + { + self.request_with_pagination_and_filter_and_sorting( + request, + pagination, + sorting, + PredicateBox::default(), + ) + } + + /// Create a request with pagination and add the filter. + /// + /// # Errors + /// Fails if sending request fails + pub fn request_with_pagination_and_filter( + &self, + request: R, + pagination: Pagination, + filter: PredicateBox, + ) -> QueryHandlerResult> + where + R: Query + Into + Debug, + >::Error: Into, // Seems redundant + { + self.request_with_pagination_and_filter_and_sorting( + request, + pagination, + Sorting::default(), + filter, + ) + } + /// Query API entry point. Requests queries from `Iroha` peers with pagination. /// /// Uses default blocking http-client. If you need some custom integration, look at diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 49be2e93214..f1498eff1ed 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -235,3 +235,103 @@ fn client_add_asset_with_name_length_more_than_limit_should_not_commit_transacti Ok(()) } + +#[test] +fn correct_pagination_assets_after_creating_new_one() -> Result<()> { + let (_rt, _peer, test_client) = ::new().start_with_runtime(); + + let sort_by_metadata_key = Name::from_str("sort")?; + + let account_id = AccountId::from_str("alice@wonderland")?; + + let mut assets = vec![]; + let mut instructions: Vec = vec![]; + + for i in 0..10 { + let asset_definition_id = AssetDefinitionId::from_str(&format!("xor{}#wonderland", i))?; + let asset_definition = AssetDefinition::store(asset_definition_id.clone()); + let mut asset_metadata = Metadata::new(); + asset_metadata.insert_with_limits( + sort_by_metadata_key.clone(), + Value::U128(i), + MetadataLimits::new(10, 22), + )?; + let asset = Asset::new( + AssetId::new(asset_definition_id, account_id.clone()), + AssetValue::Store(asset_metadata), + ); + + assets.push(asset.clone()); + + let create_asset_definition = RegisterBox::new(asset_definition); + let create_asset = RegisterBox::new(asset); + + instructions.push(create_asset_definition.into()); + instructions.push(create_asset.into()); + } + + test_client.submit_all_blocking(instructions)?; + + let sorting = Sorting::new(sort_by_metadata_key.clone()); + + let res = test_client.request_with_pagination_and_sorting( + client::asset::by_account_id(account_id.clone()), + Pagination::new(Some(1), Some(5)), + sorting.clone(), + )?; + + assert_eq!( + res.output + .iter() + .map(|asset| asset.id().definition_id.name.clone()) + .collect::>(), + assets + .iter() + .take(5) + .map(|asset| asset.id().definition_id.name.clone()) + .collect::>() + ); + + println!(); + + let new_asset_definition_id = AssetDefinitionId::from_str("xor10#wonderland")?; + let new_asset_definition = AssetDefinition::store(new_asset_definition_id.clone()); + let mut new_asset_metadata = Metadata::new(); + new_asset_metadata.insert_with_limits( + sort_by_metadata_key, + Value::U128(10), + MetadataLimits::new(10, 22), + )?; + let new_asset = Asset::new( + AssetId::new(new_asset_definition_id, account_id.clone()), + AssetValue::Store(new_asset_metadata), + ); + + let create_asset_definition = RegisterBox::new(new_asset_definition); + let create_asset = RegisterBox::new(new_asset.clone()); + + test_client.submit_all_blocking(vec![create_asset_definition.into(), create_asset.into()])?; + + let res = test_client.request_with_pagination_and_sorting( + client::asset::by_account_id(account_id), + Pagination::new(Some(6), None), + sorting, + )?; + + let mut right = assets.into_iter().skip(5).take(5).collect::>(); + + right.push(new_asset); + + assert_eq!( + res.output + .into_iter() + .map(|asset| asset.id().definition_id.name.clone()) + .collect::>(), + right + .into_iter() + .map(|asset| asset.id().definition_id.name.clone()) + .collect::>() + ); + + Ok(()) +} diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 0a0067c5329..9b864554732 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -52,6 +52,7 @@ pub mod permissions; pub mod predicate; pub mod query; pub mod role; +pub mod sorting; pub mod transaction; pub mod trigger; @@ -977,6 +978,7 @@ pub mod prelude { pagination::{prelude::*, Pagination}, peer::prelude::*, role::prelude::*, + sorting::prelude::*, trigger::prelude::*, EnumTryAsError, HasMetadata, IdBox, Identifiable, IdentifiableBox, Parameter, PredicateTrait, RegistrableBox, TryAsMut, TryAsRef, ValidationError, Value, diff --git a/data_model/src/query.rs b/data_model/src/query.rs index 1621b77131c..96049316198 100644 --- a/data_model/src/query.rs +++ b/data_model/src/query.rs @@ -990,7 +990,7 @@ pub mod asset { impl FindAllAssets { /// Construct [`FindAllAssets`]. pub const fn new() -> Self { - FindAllAssets + Self } } diff --git a/data_model/src/sorting.rs b/data_model/src/sorting.rs new file mode 100644 index 00000000000..1a6be0abf4b --- /dev/null +++ b/data_model/src/sorting.rs @@ -0,0 +1,68 @@ +//! Structures and traits related to sorting. + +#[cfg(not(feature = "std"))] +use alloc::{ + borrow::ToOwned as _, + collections::btree_map, + format, + string::{String, ToString as _}, + vec, + vec::Vec, +}; +#[cfg(feature = "std")] +use std::collections::btree_map; + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "warp")] +use warp::{Filter, Rejection}; + +use crate::prelude::*; + +const SORT_BY_KEY: &str = "sort_by_key"; + +/// Structure for sorting requests +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Sorting { + /// [`Name`] of the key in [`Asset`]'s metadata used to order query result. + pub sort_by_key: Option, +} + +impl Sorting { + /// + pub fn new(key: Name) -> Self { + Self { + sort_by_key: Some(key), + } + } +} + +impl From for btree_map::BTreeMap { + fn from(sorting: Sorting) -> Self { + let mut btree = Self::new(); + if let Some(key) = sorting.sort_by_key { + btree.insert(String::from(SORT_BY_KEY), key.to_string()); + } + btree + } +} + +impl From for Vec<(&'static str, String)> { + fn from(sorting: Sorting) -> Self { + let mut vec = Vec::new(); + if let Some(key) = sorting.sort_by_key { + vec.push((SORT_BY_KEY, key.to_string())); + } + vec + } +} + +#[cfg(feature = "warp")] +/// Filter for warp which extracts sorting +pub fn sorting() -> impl Filter + Copy { + warp::query() +} + +pub mod prelude { + //! Prelude: re-export most commonly used traits, structs and macros from this module. + pub use super::*; +}