Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion js/app/CLAUDE.md

This file was deleted.

1 change: 1 addition & 0 deletions js/app/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AGENTS.md
2 changes: 1 addition & 1 deletion rust/cloud-storage/item_filters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ impl IsEmpty for ProjectFilters {
}

/// a bundle of all of the filters for each entity type
#[derive(Debug, Clone, Default, Deserialize)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema, schemars::JsonSchema))]
pub struct EntityFilters {
/// the bundled [ProjectFilters]
Expand Down
40 changes: 40 additions & 0 deletions rust/cloud-storage/models_pagination/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ pub struct Cursor<Id, C, F> {
pub filter: F,
}

impl<Id, C, F> Cursor<Id, C, F> {
/// maps the filter from one type to another
pub fn map_filter<Fn: FnOnce(F) -> F2, F2>(self, cb: Fn) -> Cursor<Id, C, F2> {
let Cursor {
id,
limit,
val,
filter,
} = self;

Cursor {
id,
limit,
val,
filter: cb(filter),
}
}
}

/// Type alias for a [Cursor] with a [CursorVal] which is [Sortable]
pub type CursorWithVal<Id, V> = Cursor<Id, CursorVal<V>, ()>;

Expand Down Expand Up @@ -458,6 +477,27 @@ impl<I, T: Sortable, F> Query<I, T, F> {
}),
}
}
/// maps this filter type into another one via a callback fn.
/// This is analagous to [Option::map] over just the filter generic type
pub fn try_map_filter<Cb, F2, E>(self, cb: Cb) -> Result<Query<I, T, F2>, E>
where
Cb: FnOnce(F) -> Result<F2, E>,
{
match self {
Query::Sort(a, b) => Ok(Query::Sort(a, cb(b)?)),
Query::Cursor(Cursor {
id,
limit,
val,
filter,
}) => Ok(Query::Cursor(Cursor {
id,
limit,
val,
filter: cb(filter)?,
})),
}
}

/// returns the entity [Uuid] and [Sortable::Value] if they exist
pub fn vals(&self) -> (Option<&I>, Option<&T::Value>) {
Expand Down
120 changes: 103 additions & 17 deletions rust/cloud-storage/soup/src/domain/models.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use comms::domain::models::GetChannelsRequest;
use email::domain::models::{GetEmailsRequest, PreviewView};
use frecency::domain::models::{AggregateFrecency, FrecencyQueryErr};
use item_filters::ast::EntityFilterAst;
use item_filters::{
EntityFilters,
ast::{EntityFilterAst, ExpandErr},
};
use macro_user_id::user_id::MacroUserIdStr;
use model_entity::Entity;
use models_pagination::{
Expand Down Expand Up @@ -93,31 +96,112 @@ pub struct AdvancedSortParams<'a> {
}

#[derive(Debug)]
pub enum SoupQuery {
Simple(Query<Uuid, SimpleSortMethod, Option<EntityFilterAst>>),
Frecency(Query<Uuid, Frecency, Option<EntityFilterAst>>),
pub enum SoupQuery<T> {
Simple(SimpleQueryInner<T>),
Frecency(FrecencyQueryInner<T>),
}

impl SoupQuery {
pub(crate) fn filter(&self) -> Option<&EntityFilterAst> {
/// the inner private type for [SoupQuery::Simple]
#[derive(Debug)]
pub struct SimpleQueryInner<T>(pub(crate) Query<Uuid, SimpleSortMethod, T>);

/// the inner private type for [SoupQuery::Frecency]
#[derive(Debug)]
pub struct FrecencyQueryInner<T>(pub(crate) Query<Uuid, Frecency, T>);

impl SoupQuery<EntityFilters> {
/// create a new instance of a [SimpleSortMethod] with [EntityFilters] this is used to
/// construct the initial page request. To paginate an existing cursor see [Self::new_cursor_simple]
pub fn new_sort_simple(method: SimpleSortMethod, filters: EntityFilters) -> Self {
SoupQuery::Simple(SimpleQueryInner(models_pagination::Query::Sort(
method, filters,
)))
}

/// create a new instance of a [Frecency] with [EntityFilters] this is used to
/// construct the initial page request. To paginate an existing cursor see [Self::new_cursor_frecency]
pub fn new_sort_frecency(method: Frecency, filters: EntityFilters) -> Self {
SoupQuery::Frecency(FrecencyQueryInner(models_pagination::Query::Sort(
method, filters,
)))
}

/// create a new instance of a [SimpleSortMethod] with an existing cursor on [EntityFilters].
/// This is used to continue paginating on an existing cursor.
/// To create a new initial page see [Self::new_sort_simple]
pub fn new_cursor_simple(
cursor: CursorWithValAndFilter<Uuid, SimpleSortMethod, EntityFilters>,
) -> Self {
SoupQuery::Simple(SimpleQueryInner(models_pagination::Query::Cursor(cursor)))
}

/// create a new instance of a [Frecency] with an existing cursor on [EntityFilters].
/// This is used to continue paginating on an existing cursor.
/// To create a new initial page see [Self::new_sort_simple]
pub fn new_cursor_frecency(
cursor: CursorWithValAndFilter<Uuid, Frecency, EntityFilters>,
) -> Self {
SoupQuery::Frecency(FrecencyQueryInner(models_pagination::Query::Cursor(cursor)))
}

pub fn filter(&self) -> &EntityFilters {
match self {
SoupQuery::Simple(query) => query.filter().as_ref(),
SoupQuery::Frecency(query) => query.filter().as_ref(),
SoupQuery::Simple(SimpleQueryInner(query)) => query.filter(),
SoupQuery::Frecency(FrecencyQueryInner(query)) => query.filter(),
}
}

pub fn into_ast(self) -> Result<SoupQuery<Option<EntityFilterAst>>, ExpandErr> {
match self {
SoupQuery::Simple(SimpleQueryInner(query)) => Ok(SoupQuery::Simple(SimpleQueryInner(
query.try_map_filter(EntityFilterAst::new_from_filters)?,
))),
SoupQuery::Frecency(FrecencyQueryInner(query)) => Ok(SoupQuery::Frecency(
FrecencyQueryInner(query.try_map_filter(EntityFilterAst::new_from_filters)?),
)),
}
}
}

#[derive(Debug)]
pub struct SoupRequest {
pub struct SoupRequest<T> {
pub soup_type: SoupType,
pub limit: u16,
pub cursor: SoupQuery,
pub cursor: SoupQuery<T>,
pub user: MacroUserIdStr<'static>,
pub email_preview_view: PreviewView,
pub link_id: Option<Uuid>,
}

impl SoupRequest {
impl SoupRequest<EntityFilters> {
/// returns a reference to the filters to pass to the paginator
/// this is used to prevent passing back the full ast on each server request
pub(crate) fn filters(&self) -> &EntityFilters {
self.cursor.filter()
}

pub(crate) fn into_ast(self) -> Result<SoupRequest<Option<EntityFilterAst>>, ExpandErr> {
let SoupRequest {
soup_type,
limit,
cursor,
user,
email_preview_view,
link_id,
} = self;

Ok(SoupRequest {
soup_type,
limit,
cursor: cursor.into_ast()?,
user,
email_preview_view,
link_id,
})
}
}

impl SoupRequest<Option<EntityFilterAst>> {
/// take the parts of the [SoupRequest] that are only relevant to email
/// and move them into a [GetEmailsRequest] if it is possible to create one
pub(crate) fn build_email_request(&self) -> Option<GetEmailsRequest> {
Expand All @@ -127,16 +211,16 @@ impl SoupRequest {
macro_id: self.user.clone(),
limit: Some(self.limit as u32),
query: match &self.cursor {
SoupQuery::Simple(Query::Sort(t, f)) => Some(Query::Sort(
SoupQuery::Simple(SimpleQueryInner(Query::Sort(t, f))) => Some(Query::Sort(
*t,
f.as_ref().and_then(|f| f.email_filter.clone()),
)),
SoupQuery::Simple(Query::Cursor(CursorWithValAndFilter {
SoupQuery::Simple(SimpleQueryInner(Query::Cursor(CursorWithValAndFilter {
id,
limit,
val,
filter,
})) => Some(Query::Cursor(CursorWithValAndFilter {
}))) => Some(Query::Cursor(CursorWithValAndFilter {
id: *id,
limit: *limit,
val: val.clone(),
Expand All @@ -153,16 +237,16 @@ impl SoupRequest {
macro_id: self.user.clone(),
limit: Some(self.limit as u32),
query: match &self.cursor {
SoupQuery::Simple(Query::Sort(t, f)) => Some(Query::Sort(
SoupQuery::Simple(SimpleQueryInner(Query::Sort(t, f))) => Some(Query::Sort(
*t,
f.as_ref().and_then(|f| f.channel_filter.clone()),
)),
SoupQuery::Simple(Query::Cursor(CursorWithValAndFilter {
SoupQuery::Simple(SimpleQueryInner(Query::Cursor(CursorWithValAndFilter {
id,
limit,
val,
filter,
})) => Some(Query::Cursor(CursorWithValAndFilter {
}))) => Some(Query::Cursor(CursorWithValAndFilter {
id: *id,
limit: *limit,
val: val.clone(),
Expand Down Expand Up @@ -223,4 +307,6 @@ pub enum SoupErr {
EmailErr(#[from] email::domain::models::EmailErr),
#[error("A comms error has occured, see logs for more details")]
CommsErr,
#[error(transparent)]
AstErr(#[from] ExpandErr),
}
8 changes: 4 additions & 4 deletions rust/cloud-storage/soup/src/domain/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::domain::models::{
AdvancedSortParams, FrecencySoupItem, SimpleSortRequest, SoupErr, SoupRequest,
};
use either::Either;
use item_filters::ast::EntityFilterAst;
use item_filters::EntityFilters;
use models_pagination::{Frecency, PaginatedCursor, SimpleSortMethod};
use models_soup::item::SoupItem;

Expand Down Expand Up @@ -43,13 +43,13 @@ pub trait SoupRepo: Send + Sync + 'static {
/// 1. The sort method is [Either] [SimpleSortMethod] or [Frecency]
/// 1. The filter type is an [Option] [EntityFilterAst]
pub type SoupOutput = Either<
PaginatedCursor<FrecencySoupItem, String, SimpleSortMethod, Option<EntityFilterAst>>,
PaginatedCursor<FrecencySoupItem, String, Frecency, Option<EntityFilterAst>>,
PaginatedCursor<FrecencySoupItem, String, SimpleSortMethod, EntityFilters>,
PaginatedCursor<FrecencySoupItem, String, Frecency, EntityFilters>,
>;

pub trait SoupService: Send + Sync + 'static {
fn get_user_soup(
&self,
req: SoupRequest,
req: SoupRequest<EntityFilters>,
) -> impl Future<Output = Result<SoupOutput, SoupErr>> + Send;
}
19 changes: 10 additions & 9 deletions rust/cloud-storage/soup/src/domain/service.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::domain::{
models::{
AdvancedSortParams, FrecencySoupItem, SimpleSortQuery, SimpleSortRequest, SoupErr,
SoupQuery, SoupRequest, SoupType,
AdvancedSortParams, FrecencyQueryInner, FrecencySoupItem, SimpleQueryInner,
SimpleSortQuery, SimpleSortRequest, SoupErr, SoupQuery, SoupRequest, SoupType,
},
ports::{SoupOutput, SoupRepo, SoupService},
};
Expand All @@ -16,7 +16,7 @@ use frecency::domain::{
models::{AggregateId, FrecencyPageRequest, JoinFrecency},
ports::FrecencyQueryService,
};
use item_filters::ast::EntityFilterAst;
use item_filters::{EntityFilters, ast::EntityFilterAst};
use macro_user_id::{cowlike::CowLike, user_id::MacroUserIdStr};
use model_entity::as_owned::ShallowClone;
use models_pagination::{
Expand Down Expand Up @@ -347,15 +347,16 @@ where
C: ChannelsService,
{
#[tracing::instrument(err, skip(self))]
async fn get_user_soup(&self, req: SoupRequest) -> Result<SoupOutput, SoupErr> {
async fn get_user_soup(&self, req: SoupRequest<EntityFilters>) -> Result<SoupOutput, SoupErr> {
let entity_filter = req.filters().clone();
let req = req.into_ast()?;
let limit = req.limit.clamp(20, 500);
let paginate_filter = req.cursor.filter().cloned();

let email_request = req.build_email_request();
let comms_request = req.build_comms_request();

match req.cursor {
SoupQuery::Simple(cursor) => {
SoupQuery::Simple(SimpleQueryInner(cursor)) => {
let sort_method = *cursor.sort_method();

let main_soup_fut = self.handle_simple_request(
Expand All @@ -379,16 +380,16 @@ where
.chain(email_soup?)
.chain(comms_soup?)
.paginate_on(limit.into(), sort_method)
.filter_on(paginate_filter)
.filter_on(entity_filter)
.sort_desc()
.into_page(),
))
}
SoupQuery::Frecency(cursor) => Ok(Either::Right(
SoupQuery::Frecency(FrecencyQueryInner(cursor)) => Ok(Either::Right(
self.handle_advanced_sort(cursor, req.soup_type, req.user, limit)
.await?
.paginate_on(limit.into(), Frecency)
.filter_on(paginate_filter)
.filter_on(entity_filter)
.into_page(),
)),
}
Expand Down
Loading
Loading