diff --git a/cli/src/torii/routing.rs b/cli/src/torii/routing.rs index 2e86f43de06..71558aed48b 100644 --- a/cli/src/torii/routing.rs +++ b/cli/src/torii/routing.rs @@ -119,19 +119,22 @@ 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 { - ( - value.len(), - Value::Vec(value.into_iter().paginate(pagination).collect()), - ) + + let (total, result) = if let Value::Vec(vec_of_val) = result { + let len = vec_of_val.len(); + let vec_of_val = apply_sorting_and_pagination(vec_of_val, &sorting, pagination); + + (len, Value::Vec(vec_of_val)) } else { (1, result) }; + let total = total .try_into() .map_err(|e: TryFromIntError| QueryError::Conversion(e.to_string()))?; @@ -145,6 +148,35 @@ pub(crate) async fn handle_queries( Ok(Scale(paginated_result.into())) } +fn apply_sorting_and_pagination( + mut vec_of_val: Vec, + sorting: &Sorting, + pagination: Pagination, +) -> Vec { + if let Some(ref key) = sorting.sort_by_metadata_key { + let f = |value1: &Value| { + if let Value::U128(num) = value1 { + *num + } else { + 0 + } + }; + + vec_of_val.sort_by_key(|value0| match value0 { + Value::Identifiable(IdentifiableBox::Asset(asset)) => match asset.value() { + AssetValue::Store(store) => store.get(key).map_or(0, f), + _ => 0, + }, + Value::Identifiable(v) => TryInto::<&dyn HasMetadata>::try_into(v) + .map(|has_metadata| has_metadata.metadata().get(key).map_or(0, f)) + .unwrap_or(0), + _ => 0, + }); + } + + vec_of_val.into_iter().paginate(pagination).collect() +} + #[derive(serde::Serialize)] #[non_exhaustive] enum Health { @@ -482,11 +514,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..6683d2cad20 100644 --- a/cli/src/torii/tests.rs +++ b/cli/src/torii/tests.rs @@ -108,11 +108,12 @@ async fn torii_pagination() { .try_into() .expect("Failed to verify"); - let pagination = Pagination { start, limit }; + let pagination = Pagination::new(start, limit); handle_queries( Arc::clone(&torii.wsv), Arc::clone(&torii.query_judge), pagination, + Sorting::default(), 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 a7b244f4525..123e0769bc6 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, sorting, and the given 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 @@ -674,6 +723,22 @@ impl Client { self.request_with_pagination_and_filter(request, pagination, PredicateBox::default()) } + /// Query API entry point. Requests queries from `Iroha` peers with sorting. + /// + /// # Errors + /// Fails if sending request fails + pub fn request_with_sorting( + &self, + request: R, + sorting: Sorting, + ) -> QueryHandlerResult> + where + R: Query + Into + Debug, + >::Error: Into, + { + self.request_with_pagination_and_sorting(request, Pagination::default(), sorting) + } + /// Query API entry point. Requests queries from `Iroha` peers. /// /// # Errors diff --git a/client/tests/integration/mod.rs b/client/tests/integration/mod.rs index 9c10990048a..cbae044a513 100644 --- a/client/tests/integration/mod.rs +++ b/client/tests/integration/mod.rs @@ -19,6 +19,7 @@ mod queries; mod query_errors; mod restart_peer; mod roles; +mod sorting; mod transfer_asset; mod triggers; mod tx_history; diff --git a/client/tests/integration/pagination.rs b/client/tests/integration/pagination.rs index cd4f275f1b8..2da93f80a2c 100644 --- a/client/tests/integration/pagination.rs +++ b/client/tests/integration/pagination.rs @@ -31,13 +31,7 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount() { //When let vec = iroha_client - .request_with_pagination( - asset::all_definitions(), - Pagination { - start: Some(5), - limit: Some(5), - }, - ) + .request_with_pagination(asset::all_definitions(), Pagination::new(Some(5), Some(5))) .expect("Failed to get assets") .only_output(); assert_eq!(vec.len(), 5); diff --git a/client/tests/integration/sorting.rs b/client/tests/integration/sorting.rs new file mode 100644 index 00000000000..c1abf35a613 --- /dev/null +++ b/client/tests/integration/sorting.rs @@ -0,0 +1,286 @@ +#![allow(clippy::restriction, clippy::pedantic)] + +use std::str::FromStr as _; + +use iroha_client::client; +use iroha_data_model::prelude::*; +use test_network::*; + +#[test] +fn correct_pagination_assets_after_creating_new_one() { + let (_rt, _peer, test_client) = ::new().start_with_runtime(); + + let sort_by_metadata_key = Name::from_str("sort").expect("Valid"); + + let account_id = AccountId::from_str("alice@wonderland").expect("Valid"); + + 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)).expect("Valid"); + 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), + ) + .expect("Valid"); + 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) + .expect("Valid"); + + let sorting = Sorting::by_metadata_key(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(), + ) + .expect("Valid"); + + 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::>() + ); + + let new_asset_definition_id = AssetDefinitionId::from_str("xor10#wonderland").expect("Valid"); + 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), + ) + .expect("Valid"); + 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()]) + .expect("Valid"); + + let res = test_client + .request_with_pagination_and_sorting( + client::asset::by_account_id(account_id), + Pagination::new(Some(6), None), + sorting, + ) + .expect("Valid"); + + 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::>() + ); +} + +#[test] +fn correct_sorting_of_asset_definitions() { + let (_rt, _peer, test_client) = ::new().start_with_runtime(); + + let sort_by_metadata_key = Name::from_str("test_sort").expect("Valid"); + + // Test sorting asset definitions + + let mut asset_definitions = vec![]; + let mut instructions: Vec = vec![]; + + for i in 0..10 { + let asset_definition_id = + AssetDefinitionId::from_str(&format!("xor{}#wonderland", i)).expect("Valid"); + let mut asset_metadata = Metadata::new(); + asset_metadata + .insert_with_limits( + sort_by_metadata_key.clone(), + Value::U128(i), + MetadataLimits::new(10, 27), + ) + .expect("Valid"); + let asset_definition = + AssetDefinition::quantity(asset_definition_id.clone()).with_metadata(asset_metadata); + + asset_definitions.push(asset_definition.clone()); + + let create_asset_definition = RegisterBox::new(asset_definition); + instructions.push(create_asset_definition.into()); + } + + test_client + .submit_all_blocking(instructions) + .expect("Valid"); + + asset_definitions.sort_by_key(|definition| { + definition + .metadata() + .get(&sort_by_metadata_key) + .unwrap() + .clone() + }); + + let res = test_client + .request_with_sorting( + client::asset::all_definitions(), + Sorting::by_metadata_key(sort_by_metadata_key.clone()), + ) + .expect("Valid"); + + assert_eq!( + // skip rose and tulip + res.output.into_iter().skip(2).collect::>(), + asset_definitions + .into_iter() + .map(Registrable::build) + .collect::>() + ); + + // Test sorting accounts + + let mut accounts = vec![]; + let mut instructions = vec![]; + + for i in 0..10 { + let account_id = AccountId::from_str(&format!("bob{}@wonderland", i)).expect("Valid"); + let mut account_metadata = Metadata::new(); + account_metadata + .insert_with_limits( + sort_by_metadata_key.clone(), + Value::U128(i), + MetadataLimits::new(10, 27), + ) + .expect("Valid"); + let account = Account::new(account_id, []).with_metadata(account_metadata); + + accounts.push(account.clone().build()); + + let create_account = RegisterBox::new(account); + instructions.push(create_account.into()); + } + + test_client + .submit_all_blocking(instructions) + .expect("Valid"); + + accounts.sort_by_key(|account| { + account + .metadata() + .get(&sort_by_metadata_key) + .unwrap() + .clone() + }); + + let res = test_client + .request_with_sorting( + client::account::all(), + Sorting::by_metadata_key(sort_by_metadata_key.clone()), + ) + .expect("Valid"); + + let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); + let genesis_id = AccountId::from_str("genesis@genesis").expect("Valid"); + + assert_eq!( + res.output + .into_iter() + .map(|acc| acc.id().clone()) + .filter(|id| id != &alice_id && id != &genesis_id) + .collect::>(), + accounts + .into_iter() + .map(|acc| acc.id().clone()) + .collect::>() + ); + + // Test sorting domains + + let mut domains = vec![]; + let mut instructions = vec![]; + + for i in 0..10 { + let domain_id = DomainId::from_str(&format!("neverland{}", i)).expect("Valid"); + let mut domain_metadata = Metadata::new(); + domain_metadata + .insert_with_limits( + sort_by_metadata_key.clone(), + Value::U128(i), + MetadataLimits::new(10, 27), + ) + .expect("Valid"); + let domain = Domain::new(domain_id).with_metadata(domain_metadata); + + domains.push(domain.clone().build()); + + let create_account = RegisterBox::new(domain); + instructions.push(create_account.into()); + } + + test_client + .submit_all_blocking(instructions) + .expect("Valid"); + + domains.sort_by_key(|account| { + account + .metadata() + .get(&sort_by_metadata_key) + .unwrap() + .clone() + }); + + let res = test_client + .request_with_sorting( + client::domain::all(), + Sorting::by_metadata_key(sort_by_metadata_key), + ) + .expect("Valid"); + + let genesis_id = DomainId::from_str("genesis").expect("Valid"); + let wonderland_id = DomainId::from_str("wonderland").expect("Valid"); + + assert_eq!( + res.output + .into_iter() + .filter(|domain| domain.id() != &wonderland_id && domain.id() != &genesis_id) + .collect::>(), + domains + ); +} diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index fcb5d6508c7..54265a88cfe 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -58,10 +58,7 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() { let transactions = iroha_client .request_with_pagination( transaction::by_account_id(account_id.clone()), - Pagination { - start: Some(1), - limit: Some(50), - }, + Pagination::new(Some(1), Some(50)), ) .expect("Failed to get transaction history") .only_output(); diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index e29d72daaa5..fbd7c73dace 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -57,6 +57,7 @@ pub mod permissions; pub mod predicate; pub mod query; pub mod role; +pub mod sorting; pub mod transaction; pub mod trigger; @@ -404,6 +405,24 @@ impl IdentifiableBox { } } +impl<'idbox> TryFrom<&'idbox IdentifiableBox> for &'idbox dyn HasMetadata { + type Error = (); + + fn try_from( + v: &'idbox IdentifiableBox, + ) -> Result<&'idbox (dyn HasMetadata + 'idbox), Self::Error> { + match v { + IdentifiableBox::NewDomain(v) => Ok(v.as_ref()), + IdentifiableBox::NewAccount(v) => Ok(v.as_ref()), + IdentifiableBox::NewAssetDefinition(v) => Ok(v.as_ref()), + IdentifiableBox::Domain(v) => Ok(v.as_ref()), + IdentifiableBox::Account(v) => Ok(v.as_ref()), + IdentifiableBox::AssetDefinition(v) => Ok(v.as_ref()), + _ => Err(()), + } + } +} + /// Boxed [`Value`]. pub type ValueBox = Box; @@ -1015,17 +1034,11 @@ pub mod prelude { #[cfg(feature = "mutable_api")] pub use super::Registrable; pub use super::{ - account::prelude::*, - asset::prelude::*, - block_value::prelude::*, - domain::prelude::*, - name::prelude::*, - pagination::{prelude::*, Pagination}, - peer::prelude::*, - role::prelude::*, - trigger::prelude::*, - EnumTryAsError, HasMetadata, IdBox, Identifiable, IdentifiableBox, Parameter, - PredicateTrait, RegistrableBox, TryAsMut, TryAsRef, ValidationError, Value, + account::prelude::*, asset::prelude::*, block_value::prelude::*, domain::prelude::*, + name::prelude::*, pagination::prelude::*, peer::prelude::*, role::prelude::*, + sorting::prelude::*, trigger::prelude::*, EnumTryAsError, HasMetadata, IdBox, Identifiable, + IdentifiableBox, Parameter, PredicateTrait, RegistrableBox, TryAsMut, TryAsRef, + ValidationError, Value, }; pub use crate::{ events::prelude::*, expression::prelude::*, isi::prelude::*, metadata::prelude::*, diff --git a/data_model/src/query.rs b/data_model/src/query.rs index f38a4959a76..abe2d8e5ecb 100644 --- a/data_model/src/query.rs +++ b/data_model/src/query.rs @@ -1015,7 +1015,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..f7f9148e38f --- /dev/null +++ b/data_model/src/sorting.rs @@ -0,0 +1,52 @@ +//! Structures and traits related to sorting. + +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString as _}, + vec::Vec, +}; + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "warp")] +use warp::{Filter, Rejection}; + +use crate::prelude::*; + +const SORT_BY_KEY: &str = "sort_by_metadata_key"; + +/// Enum for sorting requests +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Sorting { + /// Sort query result using [`Name`] of the key in [`Asset`]'s metadata. + pub sort_by_metadata_key: Option, +} + +impl Sorting { + /// Creates a sorting by [`Name`] of the key. + pub fn by_metadata_key(key: Name) -> Self { + Self { + sort_by_metadata_key: Some(key), + } + } +} + +impl From for Vec<(&'static str, String)> { + fn from(sorting: Sorting) -> Self { + let mut vec = Vec::new(); + if let Some(key) = sorting.sort_by_metadata_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::*; +}