diff --git a/askar-storage/src/any.rs b/askar-storage/src/any.rs index c1e4e5dc0..3a0d8ad61 100644 --- a/askar-storage/src/any.rs +++ b/askar-storage/src/any.rs @@ -4,6 +4,7 @@ use std::{fmt::Debug, sync::Arc}; use super::{Backend, BackendSession, ManageBackend}; use crate::{ + backend::OrderBy, entry::{Entry, EntryKind, EntryOperation, EntryTag, Scan, TagFilter}, error::Error, future::BoxFuture, @@ -72,9 +73,12 @@ impl Backend for WrapBackend { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> BoxFuture<'_, Result, Error>> { - self.0 - .scan(profile, kind, category, tag_filter, offset, limit) + self.0.scan( + profile, kind, category, tag_filter, offset, limit, order_by, descending, + ) } #[inline] @@ -142,9 +146,12 @@ impl Backend for AnyBackend { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> BoxFuture<'_, Result, Error>> { - self.0 - .scan(profile, kind, category, tag_filter, offset, limit) + self.0.scan( + profile, kind, category, tag_filter, offset, limit, order_by, descending, + ) } #[inline] @@ -207,10 +214,13 @@ impl BackendSession for AnyBackendSession { category: Option<&'q str>, tag_filter: Option, limit: Option, + order_by: Option, + descending: bool, for_update: bool, ) -> BoxFuture<'q, Result, Error>> { - self.0 - .fetch_all(kind, category, tag_filter, limit, for_update) + self.0.fetch_all( + kind, category, tag_filter, limit, order_by, descending, for_update, + ) } /// Remove all matching records from the store diff --git a/askar-storage/src/backend/db_utils.rs b/askar-storage/src/backend/db_utils.rs index 03c3e3400..51715eeff 100644 --- a/askar-storage/src/backend/db_utils.rs +++ b/askar-storage/src/backend/db_utils.rs @@ -18,6 +18,8 @@ use crate::{ }, }; +use super::OrderBy; + /// cbindgen:ignore pub const PAGE_SIZE: usize = 32; @@ -453,6 +455,17 @@ pub trait QueryPrepare { } query } + + fn order_by_query<'q>(mut query: String, order_by: OrderBy, descending: bool) -> String { + query.push_str(" ORDER BY "); + match order_by { + OrderBy::Id => query.push_str("id"), + } + if descending { + query.push_str(" DESC"); + } + query + } } pub fn replace_arg_placeholders( @@ -625,6 +638,8 @@ pub fn extend_query<'q, Q: QueryPrepare>( tag_filter: Option<(String, Vec>)>, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> Result where i64: for<'e> Encode<'e, Q::DB> + Type, @@ -636,9 +651,16 @@ where query.push_str(" AND "); // assumes WHERE already occurs query.push_str(&filter_clause); }; - if offset.is_some() || limit.is_some() { - query = Q::limit_query(query, args, offset, limit); - }; + // Only add ordering, and limit/offset, if the query starts with SELECT + if query.trim_start().to_uppercase().starts_with("SELECT") { + if let Some(order_by_value) = order_by { + query = Q::order_by_query(query, order_by_value, descending); + }; + + if offset.is_some() || limit.is_some() { + query = Q::limit_query(query, args, offset, limit); + }; + } Ok(query) } diff --git a/askar-storage/src/backend/mod.rs b/askar-storage/src/backend/mod.rs index 36a3c2851..4393a5dbe 100644 --- a/askar-storage/src/backend/mod.rs +++ b/askar-storage/src/backend/mod.rs @@ -22,6 +22,18 @@ pub mod postgres; /// Sqlite database support pub mod sqlite; +/// Enum to support custom ordering in record queries +#[derive(Debug)] +pub enum OrderBy { + /// Order by ID field + Id, +} + +impl Default for OrderBy { + fn default() -> Self { + OrderBy::Id + } +} /// Represents a generic backend implementation pub trait Backend: Debug + Send + Sync { /// The type of session managed by this backend @@ -54,6 +66,8 @@ pub trait Backend: Debug + Send + Sync { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> BoxFuture<'_, Result, Error>>; /// Create a new session against the store @@ -122,6 +136,8 @@ pub trait BackendSession: Debug + Send { category: Option<&'q str>, tag_filter: Option, limit: Option, + order_by: Option, + descending: bool, for_update: bool, ) -> BoxFuture<'q, Result, Error>>; @@ -185,7 +201,16 @@ pub async fn copy_profile( to_profile: &str, ) -> Result<(), Error> { let scan = from_backend - .scan(Some(from_profile.into()), None, None, None, None, None) + .scan( + Some(from_profile.into()), + None, + None, + None, + None, + None, + None, + false, + ) .await?; if let Err(e) = to_backend.create_profile(Some(to_profile.into())).await { if e.kind() != ErrorKind::Duplicate { diff --git a/askar-storage/src/backend/postgres/mod.rs b/askar-storage/src/backend/postgres/mod.rs index 229fa8b1f..74241cfa2 100644 --- a/askar-storage/src/backend/postgres/mod.rs +++ b/askar-storage/src/backend/postgres/mod.rs @@ -25,6 +25,7 @@ use super::{ Backend, BackendSession, }; use crate::{ + backend::OrderBy, entry::{EncEntryTag, Entry, EntryKind, EntryOperation, EntryTag, Scan, TagFilter}, error::Error, future::{unblock, BoxFuture}, @@ -268,6 +269,8 @@ impl Backend for PostgresBackend { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> BoxFuture<'_, Result, Error>> { Box::pin(async move { let session = self.session(profile, false)?; @@ -282,6 +285,8 @@ impl Backend for PostgresBackend { tag_filter, offset, limit, + order_by, + descending, false, ); let stream = scan.then(move |enc_rows| { @@ -347,8 +352,15 @@ impl BackendSession for DbSession { }) .await?; params.push(enc_category); - let query = - extend_query::(COUNT_QUERY, &mut params, tag_filter, None, None)?; + let query = extend_query::( + COUNT_QUERY, + &mut params, + tag_filter, + None, + None, + None, + false, + )?; let mut active = acquire_session(&mut *self).await?; let count = sqlx::query_scalar_with(query.as_str(), params) .fetch_one(active.connection_mut()) @@ -424,6 +436,8 @@ impl BackendSession for DbSession { category: Option<&'q str>, tag_filter: Option, limit: Option, + order_by: Option, + descending: bool, for_update: bool, ) -> BoxFuture<'q, Result, Error>> { let category = category.map(|c| c.to_string()); @@ -440,6 +454,8 @@ impl BackendSession for DbSession { tag_filter, None, limit, + order_by, + descending, for_update, ); pin!(scan); @@ -483,6 +499,8 @@ impl BackendSession for DbSession { tag_filter, None, None, + None, + false, )?; let mut active = acquire_session(&mut *self).await?; @@ -752,6 +770,8 @@ fn perform_scan( tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, for_update: bool, ) -> impl Stream, Error>> + '_ { try_stream! { @@ -772,7 +792,7 @@ fn perform_scan( } }).await?; params.push(enc_category); - let mut query = extend_query::(SCAN_QUERY, &mut params, tag_filter, offset, limit)?; + let mut query = extend_query::(SCAN_QUERY, &mut params, tag_filter, offset, limit, order_by, descending)?; if for_update { query.push_str(" FOR NO KEY UPDATE"); } diff --git a/askar-storage/src/backend/sqlite/mod.rs b/askar-storage/src/backend/sqlite/mod.rs index 90509faf5..73f5326b2 100644 --- a/askar-storage/src/backend/sqlite/mod.rs +++ b/askar-storage/src/backend/sqlite/mod.rs @@ -24,6 +24,7 @@ use super::{ Backend, BackendSession, }; use crate::{ + backend::OrderBy, entry::{EncEntryTag, Entry, EntryKind, EntryOperation, EntryTag, Scan, TagFilter}, error::Error, future::{unblock, BoxFuture}, @@ -262,6 +263,8 @@ impl Backend for SqliteBackend { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> BoxFuture<'_, Result, Error>> { Box::pin(async move { let session = self.session(profile, false)?; @@ -276,6 +279,8 @@ impl Backend for SqliteBackend { tag_filter, offset, limit, + order_by, + descending, ); let stream = scan.then(move |enc_rows| { let category = category.clone(); @@ -330,8 +335,15 @@ impl BackendSession for DbSession { }) .await?; params.push(enc_category); - let query = - extend_query::(COUNT_QUERY, &mut params, tag_filter, None, None)?; + let query = extend_query::( + COUNT_QUERY, + &mut params, + tag_filter, + None, + None, + None, + false, + )?; let mut active = acquire_session(&mut *self).await?; let count = sqlx::query_scalar_with(query.as_str(), params) .fetch_one(active.connection_mut()) @@ -398,6 +410,8 @@ impl BackendSession for DbSession { category: Option<&'q str>, tag_filter: Option, limit: Option, + order_by: Option, + descending: bool, _for_update: bool, ) -> BoxFuture<'q, Result, Error>> { let category = category.map(|c| c.to_string()); @@ -413,6 +427,8 @@ impl BackendSession for DbSession { tag_filter, None, limit, + order_by, + descending, ); pin!(scan); let mut enc_rows = vec![]; @@ -455,6 +471,8 @@ impl BackendSession for DbSession { tag_filter, None, None, + None, + false, )?; let mut active = acquire_session(&mut *self).await?; @@ -703,6 +721,8 @@ fn perform_scan( tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> impl Stream, Error>> + '_ { try_stream! { let mut params = QueryParams::new(); @@ -720,7 +740,7 @@ fn perform_scan( } }).await?; params.push(enc_category); - let query = extend_query::(SCAN_QUERY, &mut params, tag_filter, offset, limit)?; + let query = extend_query::(SCAN_QUERY, &mut params, tag_filter, offset, limit, order_by, descending)?; let mut batch = Vec::with_capacity(PAGE_SIZE); diff --git a/askar-storage/tests/utils/mod.rs b/askar-storage/tests/utils/mod.rs index 7e2c5173c..75d36dda8 100644 --- a/askar-storage/tests/utils/mod.rs +++ b/askar-storage/tests/utils/mod.rs @@ -89,6 +89,8 @@ pub async fn db_insert_fetch(db: AnyBackend) { Some(&test_row.category), None, None, + None, + false, false, ) .await @@ -489,6 +491,8 @@ pub async fn db_scan(db: AnyBackend) { tag_filter, offset, limit, + None, + false, ) .await .expect(ERR_SCAN); @@ -506,6 +510,8 @@ pub async fn db_scan(db: AnyBackend) { tag_filter, offset, limit, + None, + false, ) .await .expect(ERR_SCAN); @@ -728,6 +734,8 @@ pub async fn db_txn_fetch_for_update(db: AnyBackend) { Some(&test_row.category), None, Some(2), + None, + false, true, ) .await @@ -879,7 +887,16 @@ pub async fn db_import_scan(db: AnyBackend) { let copy = db.create_profile(None).await.expect(ERR_PROFILE); let mut copy_conn = db.session(Some(copy.clone()), true).expect(ERR_SESSION); let records = db - .scan(None, Some(EntryKind::Item), None, None, None, None) + .scan( + None, + Some(EntryKind::Item), + None, + None, + None, + None, + None, + false, + ) .await .expect(ERR_SCAN); copy_conn @@ -889,7 +906,16 @@ pub async fn db_import_scan(db: AnyBackend) { copy_conn.close(true).await.expect(ERR_COMMIT); let mut scan = db - .scan(Some(copy), Some(EntryKind::Item), None, None, None, None) + .scan( + Some(copy), + Some(EntryKind::Item), + None, + None, + None, + None, + None, + false, + ) .await .expect(ERR_SCAN); diff --git a/src/ffi/store.rs b/src/ffi/store.rs index 1c41509e1..b9b6fb684 100644 --- a/src/ffi/store.rs +++ b/src/ffi/store.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, ffi::CString, os::raw::c_char, ptr, str::FromStr, sync::Arc}; +use askar_storage::backend::OrderBy; use async_lock::{Mutex as TryMutex, MutexGuardArc as TryMutexGuard, RwLock}; use ffi_support::{rust_string_to_c, ByteBuffer, FfiStr}; use once_cell::sync::Lazy; @@ -547,9 +548,19 @@ pub extern "C" fn askar_scan_start( tag_filter: FfiStr<'_>, offset: i64, limit: i64, + order_by: FfiStr<'_>, + descending: i8, cb: Option, cb_id: CallbackId, ) -> ErrorCode { + let order_by_str = order_by.as_opt_str().map(|s| s.to_lowercase()); + let order_by = match order_by_str.as_deref() { + Some("id") => Some(OrderBy::Id), + Some(_) => return ErrorCode::Unsupported, + None => None, + }; + let descending = descending != 0; // Convert to bool + catch_err! { trace!("Scan store start"); let cb = cb.ok_or_else(|| err_msg!("No callback provided"))?; @@ -568,7 +579,7 @@ pub extern "C" fn askar_scan_start( spawn_ok(async move { let result = async { let store = handle.load().await?; - let scan = store.scan(profile, category, tag_filter, Some(offset), if limit < 0 { None }else {Some(limit)}).await?; + let scan = store.scan(profile, category, tag_filter, Some(offset), if limit < 0 { None }else {Some(limit)}, order_by, descending).await?; Ok(FFI_SCANS.insert(handle, scan).await) }.await; cb.resolve(result); @@ -733,10 +744,20 @@ pub extern "C" fn askar_session_fetch_all( category: FfiStr<'_>, tag_filter: FfiStr<'_>, limit: i64, + order_by: FfiStr<'_>, + descending: i8, for_update: i8, cb: Option, cb_id: CallbackId, ) -> ErrorCode { + let order_by_str = order_by.as_opt_str().map(|s| s.to_lowercase()); + let order_by = match order_by_str.as_deref() { + Some("id") => Some(OrderBy::Id), + Some(_) => return ErrorCode::Unsupported, + None => None, + }; + let descending = descending != 0; // Convert to bool + catch_err! { trace!("Count from store"); let cb = cb.ok_or_else(|| err_msg!("No callback provided"))?; @@ -755,7 +776,7 @@ pub extern "C" fn askar_session_fetch_all( spawn_ok(async move { let result = async { let mut session = FFI_SESSIONS.borrow(handle).await?; - session.fetch_all(category.as_deref(), tag_filter, limit, for_update != 0).await + session.fetch_all(category.as_deref(), tag_filter, limit, order_by, descending, for_update != 0).await }.await; cb.resolve(result); }); diff --git a/src/store.rs b/src/store.rs index 59e024e4d..e6476f381 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,4 +1,4 @@ -use askar_storage::backend::copy_profile; +use askar_storage::backend::{copy_profile, OrderBy}; use crate::{ error::Error, @@ -125,6 +125,8 @@ impl Store { tag_filter: Option, offset: Option, limit: Option, + order_by: Option, + descending: bool, ) -> Result, Error> { Ok(self .0 @@ -135,6 +137,8 @@ impl Store { tag_filter, offset, limit, + order_by, + descending, ) .await?) } @@ -220,6 +224,8 @@ impl Session { category: Option<&str>, tag_filter: Option, limit: Option, + order_by: Option, + descending: bool, for_update: bool, ) -> Result, Error> { Ok(self @@ -229,6 +235,8 @@ impl Session { category, tag_filter, limit, + order_by, + descending, for_update, ) .await?) @@ -449,6 +457,8 @@ impl Session { Some(KmsCategory::CryptoKey.as_str()), tag_filter, limit, + None, + false, for_update, ) .await?; diff --git a/wrappers/javascript/packages/aries-askar-nodejs/src/NodeJSAriesAskar.ts b/wrappers/javascript/packages/aries-askar-nodejs/src/NodeJSAriesAskar.ts index 8f221e302..83a89c9eb 100644 --- a/wrappers/javascript/packages/aries-askar-nodejs/src/NodeJSAriesAskar.ts +++ b/wrappers/javascript/packages/aries-askar-nodejs/src/NodeJSAriesAskar.ts @@ -787,7 +787,8 @@ export class NodeJSAriesAskar implements AriesAskar { } public async scanStart(options: ScanStartOptions): Promise { - const { category, limit, offset, profile, storeHandle, tagFilter } = serializeArguments(options) + const { category, descending, limit, offset, orderBy, profile, storeHandle, tagFilter } = + serializeArguments(options) const handle = await this.promisifyWithResponse( (cb, cbId) => this.nativeAriesAskar.askar_scan_start( @@ -797,6 +798,8 @@ export class NodeJSAriesAskar implements AriesAskar { tagFilter, +offset || 0, +limit || -1, + orderBy, + descending, cb, cbId, ), @@ -835,7 +838,7 @@ export class NodeJSAriesAskar implements AriesAskar { } public async sessionFetchAll(options: SessionFetchAllOptions): Promise { - const { forUpdate, sessionHandle, tagFilter, limit, category } = serializeArguments(options) + const { forUpdate, sessionHandle, tagFilter, limit, orderBy, descending, category } = serializeArguments(options) const handle = await this.promisifyWithResponse( (cb, cbId) => @@ -844,6 +847,8 @@ export class NodeJSAriesAskar implements AriesAskar { category, tagFilter, +limit || -1, + orderBy, + descending, forUpdate, cb, cbId, diff --git a/wrappers/javascript/packages/aries-askar-nodejs/src/library/bindings.ts b/wrappers/javascript/packages/aries-askar-nodejs/src/library/bindings.ts index e1007d5e9..d2730cbb9 100644 --- a/wrappers/javascript/packages/aries-askar-nodejs/src/library/bindings.ts +++ b/wrappers/javascript/packages/aries-askar-nodejs/src/library/bindings.ts @@ -136,7 +136,18 @@ export const nativeBindings = { askar_scan_next: [FFI_ERROR_CODE, [FFI_SCAN_HANDLE, FFI_CALLBACK_PTR, FFI_CALLBACK_ID]], askar_scan_start: [ FFI_ERROR_CODE, - [FFI_STORE_HANDLE, FFI_STRING, FFI_STRING, FFI_STRING, FFI_INT64, FFI_INT64, FFI_CALLBACK_PTR, FFI_CALLBACK_ID], + [ + FFI_STORE_HANDLE, + FFI_STRING, + FFI_STRING, + FFI_STRING, + FFI_INT64, + FFI_INT64, + FFI_STRING, + FFI_INT8, + FFI_CALLBACK_PTR, + FFI_CALLBACK_ID, + ], ], askar_session_close: [FFI_ERROR_CODE, [FFI_SESSION_HANDLE, FFI_INT8, FFI_CALLBACK_PTR, FFI_CALLBACK_ID]], @@ -150,7 +161,17 @@ export const nativeBindings = { ], askar_session_fetch_all: [ FFI_ERROR_CODE, - [FFI_SESSION_HANDLE, FFI_STRING, FFI_STRING, FFI_INT64, FFI_INT8, FFI_CALLBACK_PTR, FFI_CALLBACK_ID], + [ + FFI_SESSION_HANDLE, + FFI_STRING, + FFI_STRING, + FFI_INT64, + FFI_STRING, + FFI_INT8, + FFI_INT8, + FFI_CALLBACK_PTR, + FFI_CALLBACK_ID, + ], ], askar_session_fetch_all_keys: [ FFI_ERROR_CODE, diff --git a/wrappers/javascript/packages/aries-askar-react-native/cpp/ariesAskar.cpp b/wrappers/javascript/packages/aries-askar-react-native/cpp/ariesAskar.cpp index 864877e80..b70646a6c 100644 --- a/wrappers/javascript/packages/aries-askar-react-native/cpp/ariesAskar.cpp +++ b/wrappers/javascript/packages/aries-askar-react-native/cpp/ariesAskar.cpp @@ -333,6 +333,8 @@ jsi::Value sessionFetchAll(jsi::Runtime &rt, jsi::Object options) { auto category = jsiToValue(rt, options, "category"); auto tagFilter = jsiToValue(rt, options, "tagFilter", true); int64_t limit = jsiToValue(rt, options, "limit", true); + auto orderBy = jsiToValue(rt, options, "orderBy", true); + auto descending = jsiToValue(rt, options, "descending"); int8_t forUpdate = jsiToValue(rt, options, "forUpdate"); jsi::Function cb = options.getPropertyAsFunction(rt, "cb"); @@ -341,8 +343,8 @@ jsi::Value sessionFetchAll(jsi::Runtime &rt, jsi::Object options) { ErrorCode code = askar_session_fetch_all( sessionHandle, category.c_str(), - tagFilter.length() ? tagFilter.c_str() : nullptr, limit, forUpdate, - callbackWithResponse, CallbackId(state)); + tagFilter.length() ? tagFilter.c_str() : nullptr, limit, orderBy, descending, + forUpdate, callbackWithResponse, CallbackId(state)); return createReturnValue(rt, code, nullptr); } @@ -502,6 +504,8 @@ jsi::Value scanStart(jsi::Runtime &rt, jsi::Object options) { auto profile = jsiToValue(rt, options, "profile", true); auto offset = jsiToValue(rt, options, "offset", true); auto limit = jsiToValue(rt, options, "limit", true); + auto orderBy = jsiToValue(rt, options, "orderBy", true); + auto descending = jsiToValue(rt, options, "descending"); jsi::Function cb = options.getPropertyAsFunction(rt, "cb"); State *state = new State(&cb); @@ -510,7 +514,7 @@ jsi::Value scanStart(jsi::Runtime &rt, jsi::Object options) { ErrorCode code = askar_scan_start( storeHandle, profile.length() ? profile.c_str() : nullptr, category.c_str(), tagFilter.length() ? tagFilter.c_str() : nullptr, - offset, limit, callbackWithResponse, CallbackId(state)); + offset, limit, orderBy, descending, callbackWithResponse, CallbackId(state)); return createReturnValue(rt, code, nullptr); }; diff --git a/wrappers/javascript/packages/aries-askar-react-native/cpp/include/libaries_askar.h b/wrappers/javascript/packages/aries-askar-react-native/cpp/include/libaries_askar.h index 103dc47fe..3171e1297 100644 --- a/wrappers/javascript/packages/aries-askar-react-native/cpp/include/libaries_askar.h +++ b/wrappers/javascript/packages/aries-askar-react-native/cpp/include/libaries_askar.h @@ -440,6 +440,8 @@ ErrorCode askar_scan_start(StoreHandle handle, FfiStr tag_filter, int64_t offset, int64_t limit, + FfiStr order_by, + int8_t descending, void (*cb)(CallbackId cb_id, ErrorCode err, ScanHandle handle), CallbackId cb_id); @@ -465,6 +467,8 @@ ErrorCode askar_session_fetch_all(SessionHandle handle, FfiStr category, FfiStr tag_filter, int64_t limit, + FfiStr order_by, + int8_t descending, int8_t for_update, void (*cb)(CallbackId cb_id, ErrorCode err, diff --git a/wrappers/javascript/packages/aries-askar-react-native/src/ReactNativeAriesAskar.ts b/wrappers/javascript/packages/aries-askar-react-native/src/ReactNativeAriesAskar.ts index c519f26bb..6209a4d17 100644 --- a/wrappers/javascript/packages/aries-askar-react-native/src/ReactNativeAriesAskar.ts +++ b/wrappers/javascript/packages/aries-askar-react-native/src/ReactNativeAriesAskar.ts @@ -517,10 +517,19 @@ export class ReactNativeAriesAskar implements AriesAskar { } public async sessionFetchAll(options: SessionFetchAllOptions) { - const { category, sessionHandle, forUpdate, limit, tagFilter } = serializeArguments(options) + const { category, sessionHandle, forUpdate, limit, orderBy, descending, tagFilter } = serializeArguments(options) const handle = await this.promisifyWithResponse((cb) => this.handleError( - this.ariesAskar.sessionFetchAll({ cb, category, sessionHandle, forUpdate, limit: limit || -1, tagFilter }), + this.ariesAskar.sessionFetchAll({ + cb, + category, + sessionHandle, + forUpdate, + limit: limit || -1, + orderBy, + descending, + tagFilter, + }), ), ) diff --git a/wrappers/javascript/packages/aries-askar-shared/src/ariesAskar/AriesAskar.ts b/wrappers/javascript/packages/aries-askar-shared/src/ariesAskar/AriesAskar.ts index 06d9d5ae2..f24063a14 100644 --- a/wrappers/javascript/packages/aries-askar-shared/src/ariesAskar/AriesAskar.ts +++ b/wrappers/javascript/packages/aries-askar-shared/src/ariesAskar/AriesAskar.ts @@ -211,6 +211,8 @@ export type ScanStartOptions = { tagFilter?: Record offset?: number limit?: number + orderBy?: string + descending: boolean } export type SessionCloseOptions = { @@ -233,6 +235,8 @@ export type SessionFetchAllOptions = { category: string tagFilter?: Record limit?: number + orderBy?: string + descending: boolean forUpdate: boolean } export type SessionFetchAllKeysOptions = { diff --git a/wrappers/javascript/packages/aries-askar-shared/src/store/Scan.ts b/wrappers/javascript/packages/aries-askar-shared/src/store/Scan.ts index fb9ca2144..529b11800 100644 --- a/wrappers/javascript/packages/aries-askar-shared/src/store/Scan.ts +++ b/wrappers/javascript/packages/aries-askar-shared/src/store/Scan.ts @@ -16,11 +16,15 @@ export class Scan { private tagFilter?: Record private offset?: number private limit?: number + private orderBy?: string + private descending: boolean public constructor({ category, limit, offset, + orderBy, + descending, profile, tagFilter, store, @@ -30,6 +34,8 @@ export class Scan { tagFilter?: Record offset?: number limit?: number + orderBy?: string + descending: boolean store: Store }) { this.category = category @@ -37,6 +43,8 @@ export class Scan { this.tagFilter = tagFilter this.offset = offset this.limit = limit + this.orderBy = orderBy + this.descending = descending this.store = store } @@ -51,6 +59,8 @@ export class Scan { storeHandle: this.store.handle, limit: this.limit, offset: this.offset, + orderBy: this.orderBy, + descending: this.descending, tagFilter: this.tagFilter, profile: this.profile, category: this.category, diff --git a/wrappers/javascript/packages/aries-askar-shared/src/store/Session.ts b/wrappers/javascript/packages/aries-askar-shared/src/store/Session.ts index 8d598f6c4..493b7bef8 100644 --- a/wrappers/javascript/packages/aries-askar-shared/src/store/Session.ts +++ b/wrappers/javascript/packages/aries-askar-shared/src/store/Session.ts @@ -61,12 +61,16 @@ export class Session { category, forUpdate = false, limit, + orderBy, + descending = false, tagFilter, isJson, }: { category: string tagFilter?: Record limit?: number + orderBy?: string + descending: boolean forUpdate?: boolean isJson?: boolean }) { @@ -74,6 +78,8 @@ export class Session { const handle = await ariesAskar.sessionFetchAll({ forUpdate, limit, + orderBy, + descending, tagFilter, sessionHandle: this.handle, category, diff --git a/wrappers/javascript/packages/aries-askar-shared/src/store/Store.ts b/wrappers/javascript/packages/aries-askar-shared/src/store/Store.ts index 1b4fd134c..ac442674b 100644 --- a/wrappers/javascript/packages/aries-askar-shared/src/store/Store.ts +++ b/wrappers/javascript/packages/aries-askar-shared/src/store/Store.ts @@ -120,6 +120,8 @@ export class Store { tagFilter?: Record offset?: number limit?: number + orderBy?: string + descending: boolean profile?: string }) { return new Scan({ ...options, store: this }) diff --git a/wrappers/python/aries_askar/bindings/__init__.py b/wrappers/python/aries_askar/bindings/__init__.py index 065a8273e..040175c51 100644 --- a/wrappers/python/aries_askar/bindings/__init__.py +++ b/wrappers/python/aries_askar/bindings/__init__.py @@ -308,16 +308,20 @@ async def session_fetch_all( category: Optional[str] = None, tag_filter: Optional[Union[str, dict]] = None, limit: Optional[int] = None, + order_by: Optional[str] = None, + descending: bool = False, for_update: bool = False, ) -> EntryListHandle: """Fetch all matching rows in the Store.""" return await invoke_async( "askar_session_fetch_all", - (SessionHandle, FfiStr, FfiJson, c_int64, c_int8), + (SessionHandle, FfiStr, FfiJson, c_int64, FfiStr, c_int8, c_int8), handle, category, tag_filter, limit if limit is not None else -1, + order_by, + descending, for_update, return_type=EntryListHandle, ) @@ -455,17 +459,21 @@ async def scan_start( tag_filter: Optional[Union[str, dict]] = None, offset: Optional[int] = None, limit: Optional[int] = None, + order_by: Optional[str] = None, + descending: bool = False, ) -> ScanHandle: """Create a new Scan against the Store.""" return await invoke_async( "askar_scan_start", - (StoreHandle, FfiStr, FfiStr, FfiJson, c_int64, c_int64), + (StoreHandle, FfiStr, FfiStr, FfiJson, c_int64, c_int64, FfiStr, c_int8), handle, profile, category, tag_filter, offset or 0, limit if limit is not None else -1, + order_by, + descending, return_type=ScanHandle, ) diff --git a/wrappers/python/aries_askar/store.py b/wrappers/python/aries_askar/store.py index f92002a6b..4030a9f01 100644 --- a/wrappers/python/aries_askar/store.py +++ b/wrappers/python/aries_askar/store.py @@ -247,9 +247,20 @@ def __init__( tag_filter: Union[str, dict] = None, offset: int = None, limit: int = None, + order_by: Optional[str] = None, + descending: bool = False, ): """Initialize the Scan instance.""" - self._params = (store, profile, category, tag_filter, offset, limit) + self._params = ( + store, + profile, + category, + tag_filter, + offset, + limit, + order_by, + descending, + ) self._handle: ScanHandle = None self._buffer: IterEntryList = None @@ -265,14 +276,30 @@ def __aiter__(self): async def __anext__(self): """Fetch the next scan result during async iteration.""" if self._handle is None: - (store, profile, category, tag_filter, offset, limit) = self._params + ( + store, + profile, + category, + tag_filter, + offset, + limit, + order_by, + descending, + ) = self._params self._params = None if not store.handle: raise AskarError( AskarErrorCode.WRAPPER, "Cannot scan from closed store" ) self._handle = await bindings.scan_start( - store.handle, profile, category, tag_filter, offset, limit + store.handle, + profile, + category, + tag_filter, + offset, + limit, + order_by, + descending, ) list_handle = await bindings.scan_next(self._handle) self._buffer = iter(EntryList(list_handle)) if list_handle else None @@ -428,9 +455,13 @@ def scan( offset: int = None, limit: int = None, profile: str = None, + order_by: Optional[str] = None, + descending: bool = False, ) -> Scan: """Start a new record scan.""" - return Scan(self, profile, category, tag_filter, offset, limit) + return Scan( + self, profile, category, tag_filter, offset, limit, order_by, descending + ) def session(self, profile: str = None) -> "OpenSession": """Open a new session on the store without starting a transaction.""" @@ -517,6 +548,8 @@ async def fetch_all( tag_filter: Union[str, dict] = None, limit: int = None, *, + order_by: Optional[str] = None, + descending: bool = False, for_update: bool = False, ) -> EntryList: """Fetch all records matching a category and tag filter.""" @@ -524,7 +557,13 @@ async def fetch_all( raise AskarError(AskarErrorCode.WRAPPER, "Cannot fetch from closed session") return EntryList( await bindings.session_fetch_all( - self._handle, category, tag_filter, limit, for_update + self._handle, + category, + tag_filter, + limit, + order_by, + descending, + for_update, ) )