Database schema optimization - part 2
This commit is contained in:
@@ -8,7 +8,7 @@ use calcard::jscalendar::JSCalendarProperty;
|
||||
use common::Server;
|
||||
use jmap_proto::error::set::SetError;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::CalendarField, id::Id};
|
||||
use types::{collection::Collection, field::CalendarEventField, id::Id};
|
||||
|
||||
pub mod copy;
|
||||
pub mod get;
|
||||
@@ -67,7 +67,7 @@ pub(super) async fn assert_is_unique_uid(
|
||||
.document_exists(
|
||||
account_id,
|
||||
Collection::CalendarEvent,
|
||||
CalendarField::Uid,
|
||||
CalendarEventField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::JmapCacheState;
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use calcard::{common::timezone::Tz, jscalendar::JSCalendarDateTime};
|
||||
use chrono::offset::TimeZone;
|
||||
use common::{Server, auth::AccessToken};
|
||||
@@ -14,15 +14,17 @@ use jmap_proto::{
|
||||
object::calendar_event::{self, CalendarEventComparator, CalendarEventFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use nlp::tokenizers::word::WordTokenizer;
|
||||
use nlp::language::Language;
|
||||
use std::{cmp::Ordering, sync::Arc};
|
||||
use store::{backend::MAX_TOKEN_LENGTH, roaring::RoaringBitmap, search::SearchFilter};
|
||||
use store::{
|
||||
roaring::RoaringBitmap,
|
||||
search::{CalendarSearchField, SearchComparator, SearchFilter},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
TimeRange,
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::CalendarField,
|
||||
};
|
||||
|
||||
pub trait CalendarEventQuery: Sync + Send {
|
||||
@@ -44,10 +46,7 @@ impl CalendarEventQuery for Server {
|
||||
let cache = self
|
||||
.fetch_dav_resources(access_token, account_id, SyncCollection::Calendar)
|
||||
.await?;
|
||||
let filter_mask = (access_token.is_shared(account_id))
|
||||
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
|
||||
let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC);
|
||||
let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
|
||||
let mut filter: Option<TimeRange> = None;
|
||||
let mut did_filter_by_time = false;
|
||||
|
||||
@@ -73,15 +72,73 @@ impl CalendarEventQuery for Server {
|
||||
)))
|
||||
}
|
||||
CalendarEventFilter::Uid(uid) => {
|
||||
filters.push(SearchFilter::eq(CalendarField::Uid, uid.into_bytes()))
|
||||
filters.push(SearchFilter::eq(CalendarSearchField::Uid, uid));
|
||||
}
|
||||
CalendarEventFilter::Text(value) => {
|
||||
for token in WordTokenizer::new(&value, MAX_TOKEN_LENGTH) {
|
||||
filters.push(SearchFilter::eq(
|
||||
CalendarField::Text,
|
||||
token.word.into_owned().into_bytes(),
|
||||
));
|
||||
}
|
||||
let (text, language) =
|
||||
Language::detect(value, self.core.jmap.default_language);
|
||||
filters.push(SearchFilter::Or);
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Title,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Description,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Location,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Owner,
|
||||
text.clone(),
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Attendee,
|
||||
text,
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
CalendarEventFilter::Title(title) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Title,
|
||||
title,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Description(description) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Description,
|
||||
description,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Location(location) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
CalendarSearchField::Location,
|
||||
location,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Owner(owner) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Owner,
|
||||
owner,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::Attendee(attendee) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
CalendarSearchField::Attendee,
|
||||
attendee,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
CalendarEventFilter::After(_) | CalendarEventFilter::Before(_) => {
|
||||
if let Some(filter) = &filter
|
||||
@@ -105,29 +162,74 @@ impl CalendarEventQuery for Server {
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_set = self
|
||||
.filter(account_id, Collection::CalendarEvent, filters)
|
||||
let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
|
||||
let comparators = if !expand_recurrences {
|
||||
request
|
||||
.sort
|
||||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|comparator| match comparator.property {
|
||||
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
|
||||
Ok(SearchComparator::field(
|
||||
CalendarSearchField::Start,
|
||||
comparator.is_ascending,
|
||||
))
|
||||
}
|
||||
CalendarEventComparator::Uid => Ok(SearchComparator::field(
|
||||
CalendarSearchField::Uid,
|
||||
comparator.is_ascending,
|
||||
)),
|
||||
CalendarEventComparator::Created | CalendarEventComparator::Updated => {
|
||||
Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(comparator.property.into_string().into_owned()))
|
||||
}
|
||||
CalendarEventComparator::_T(other) => Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(other.to_string())),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(account_id, Collection::CalendarEvent, filters, comparators)
|
||||
.await?;
|
||||
|
||||
if let Some(filter_mask) = filter_mask {
|
||||
result_set.apply_mask(filter_mask);
|
||||
}
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
let num_results = result_set.results.len() as usize;
|
||||
if num_results > 0 {
|
||||
if !results.is_empty() {
|
||||
// Extract comparators
|
||||
let comparators = request
|
||||
.sort
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_default();
|
||||
let filter_mask = (access_token.is_shared(account_id))
|
||||
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
|
||||
|
||||
if expand_recurrences {
|
||||
let Some(time_range) = filter.filter(|f| f.start != i64::MIN && f.end != i64::MAX)
|
||||
else {
|
||||
@@ -136,12 +238,19 @@ impl CalendarEventQuery for Server {
|
||||
));
|
||||
};
|
||||
let max_instances = self.core.groupware.max_ical_instances;
|
||||
let mut results = Vec::with_capacity(result_set.results.len() as usize);
|
||||
let mut expanded_results = Vec::with_capacity(results.len() as usize);
|
||||
let has_uid_comparator = comparators
|
||||
.iter()
|
||||
.any(|c| matches!(c.property, CalendarEventComparator::Uid));
|
||||
|
||||
for document_id in result_set.results {
|
||||
for document_id in results {
|
||||
if filter_mask
|
||||
.as_ref()
|
||||
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(_calendar_event) = self
|
||||
.archive(account_id, Collection::CalendarEvent, document_id)
|
||||
.await?
|
||||
@@ -171,14 +280,14 @@ impl CalendarEventQuery for Server {
|
||||
.expand(default_tz, time_range)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if results.len() < max_instances {
|
||||
results.push(SearchResult {
|
||||
if expanded_results.len() < max_instances {
|
||||
expanded_results.push(SearchResult {
|
||||
created: calendar_event.created.to_native().to_be_bytes(),
|
||||
updated: calendar_event.modified.to_native().to_be_bytes(),
|
||||
start: expansion.start.to_be_bytes(),
|
||||
uid: uid.clone(),
|
||||
document_id,
|
||||
expansion_id: expansion.expansion_id,
|
||||
expansion_id: expansion.expansion_id.into(),
|
||||
});
|
||||
} else {
|
||||
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
|
||||
@@ -189,8 +298,8 @@ impl CalendarEventQuery for Server {
|
||||
}
|
||||
|
||||
// Sort results
|
||||
if !results.is_empty() {
|
||||
results.sort_by(|a, b| {
|
||||
if !expanded_results.is_empty() {
|
||||
expanded_results.sort_by(|a, b| {
|
||||
for comparator in comparators {
|
||||
let ordering = a
|
||||
.get_property(&comparator.property)
|
||||
@@ -208,69 +317,30 @@ impl CalendarEventQuery for Server {
|
||||
}
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
|
||||
// Add results
|
||||
let (mut response, paginate) = self
|
||||
.build_query_response(results.len(), cache.get_state(false), &request)
|
||||
.await?;
|
||||
if let Some(mut paginate) = paginate {
|
||||
for result in results {
|
||||
if !paginate.add(result.expansion_id + 1, result.document_id) {
|
||||
// Add results
|
||||
for result in expanded_results {
|
||||
if !response.add(result.expansion_id.unwrap() + 1, result.document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
response.update_results(paginate.build())?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
} else {
|
||||
let mut comparators_ = Vec::with_capacity(comparators.len());
|
||||
|
||||
for comparator in comparators {
|
||||
comparators_.push(match &comparator.property {
|
||||
CalendarEventComparator::Uid => {
|
||||
SearchComparator::field(CalendarField::Uid, comparator.is_ascending)
|
||||
}
|
||||
CalendarEventComparator::Start => {
|
||||
SearchComparator::field(CalendarField::Start, comparator.is_ascending)
|
||||
}
|
||||
CalendarEventComparator::Created => {
|
||||
SearchComparator::field(CalendarField::Created, comparator.is_ascending)
|
||||
}
|
||||
CalendarEventComparator::Updated => {
|
||||
SearchComparator::field(CalendarField::Updated, comparator.is_ascending)
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported.clone().into_string()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort results
|
||||
let (response, paginate) = self
|
||||
.build_query_response(num_results, cache.get_state(false), &request)
|
||||
.await?;
|
||||
if let Some(paginate) = paginate {
|
||||
self.sort(result_set, comparators_, paginate, response)
|
||||
.await
|
||||
} else {
|
||||
Ok(response)
|
||||
for document_id in results {
|
||||
if filter_mask
|
||||
.as_ref()
|
||||
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (response, _) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +352,7 @@ fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option<i64> {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SearchResult {
|
||||
expansion_id: u32,
|
||||
expansion_id: Option<u32>,
|
||||
document_id: u32,
|
||||
start: [u8; std::mem::size_of::<i64>()],
|
||||
created: [u8; std::mem::size_of::<i64>()],
|
||||
|
||||
@@ -4,21 +4,28 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{ changes::state::JmapCacheState};
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::calendar_event_notification::{
|
||||
CalendarEventNotification, CalendarEventNotificationComparator,
|
||||
CalendarEventNotificationFilter,
|
||||
},
|
||||
request::IntoValid,
|
||||
};
|
||||
use store::{SerializeInfallible, query};
|
||||
use store::{
|
||||
IterateParams, U32_LEN, U64_LEN, ValueKey,
|
||||
ahash::AHashSet,
|
||||
roaring::RoaringBitmap,
|
||||
search::SearchFilter,
|
||||
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::CalendarField,
|
||||
field::CalendarNotificationField,
|
||||
};
|
||||
|
||||
pub trait CalendarEventNotificationQuery: Sync + Send {
|
||||
@@ -29,6 +36,12 @@ pub trait CalendarEventNotificationQuery: Sync + Send {
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
struct Notification {
|
||||
document_id: u32,
|
||||
created: u64,
|
||||
event_id: u32,
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationQuery for Server {
|
||||
async fn calendar_event_notification_query(
|
||||
&self,
|
||||
@@ -44,36 +57,73 @@ impl CalendarEventNotificationQuery for Server {
|
||||
SyncCollection::CalendarEventNotification,
|
||||
)
|
||||
.await?;
|
||||
let mut notifications = Vec::with_capacity(16);
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: CalendarNotificationField::CreatedToId.into(),
|
||||
value: 0,
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: CalendarNotificationField::CreatedToId.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
notifications.push(Notification {
|
||||
document_id: key.deserialize_be_u32(key.len() - U32_LEN)?,
|
||||
created: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
|
||||
event_id: value.deserialize_be_u32(0)?,
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
CalendarEventNotificationFilter::Before(before) => {
|
||||
filters.push(SearchFilter::lt(
|
||||
CalendarField::Created,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
))
|
||||
let before = before.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
notifications
|
||||
.iter()
|
||||
.filter_map(|n| (n.created < before).then_some(n.document_id)),
|
||||
)))
|
||||
}
|
||||
CalendarEventNotificationFilter::After(after) => {
|
||||
filters.push(SearchFilter::gt(
|
||||
CalendarField::Created,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
))
|
||||
let after = after.timestamp() as u64;
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
notifications
|
||||
.iter()
|
||||
.filter_map(|n| (n.created > after).then_some(n.document_id)),
|
||||
)))
|
||||
}
|
||||
CalendarEventNotificationFilter::CalendarEventIds(ids) => {
|
||||
let has_many = ids.len() > 1;
|
||||
if has_many {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
for id in ids.into_valid() {
|
||||
filters.push(SearchFilter::eq(
|
||||
CalendarField::EventId,
|
||||
id.document_id().serialize(),
|
||||
));
|
||||
}
|
||||
if has_many {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
notifications
|
||||
.iter()
|
||||
.filter_map(|n| ids.contains(&n.event_id).then_some(n.document_id)),
|
||||
)))
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
@@ -81,49 +131,67 @@ impl CalendarEventNotificationQuery for Server {
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_set = self
|
||||
.filter(account_id, Collection::CalendarEventNotification, filters)
|
||||
.await?;
|
||||
// Parse sort criteria
|
||||
let mut is_ascending = true;
|
||||
for comparator in request.sort.take().unwrap_or_default() {
|
||||
match comparator.property {
|
||||
CalendarEventNotificationComparator::Created => {
|
||||
is_ascending = comparator.is_ascending;
|
||||
}
|
||||
CalendarEventNotificationComparator::_T(unsupported) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported));
|
||||
}
|
||||
};
|
||||
}
|
||||
if !is_ascending {
|
||||
notifications.reverse();
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(
|
||||
account_id,
|
||||
Collection::CalendarEventNotification,
|
||||
filters,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request.sort.filter(|s| !s.is_empty()).unwrap_or_else(|| {
|
||||
vec![Comparator::descending(
|
||||
CalendarEventNotificationComparator::Created,
|
||||
)]
|
||||
}) {
|
||||
comparators.push(match comparator.property {
|
||||
CalendarEventNotificationComparator::Created => {
|
||||
SearchComparator::field(CalendarField::Created, comparator.is_ascending)
|
||||
}
|
||||
CalendarEventNotificationComparator::_T(unsupported) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported));
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
if !results.is_empty() {
|
||||
let results = results.into_iter().collect::<AHashSet<_>>();
|
||||
for notification in notifications {
|
||||
if results.contains(¬ification.document_id)
|
||||
&& !response.add(0, notification.document_id)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
use calcard::jscontact::JSContactProperty;
|
||||
use common::{DavName, DavResources, Server};
|
||||
use jmap_proto::error::set::SetError;
|
||||
use store::SearchFilter;
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::ContactField, id::Id};
|
||||
|
||||
@@ -26,15 +25,15 @@ pub(super) async fn assert_is_unique_uid(
|
||||
) -> trc::Result<Result<(), SetError<JSContactProperty<Id>>>> {
|
||||
if let Some(uid) = uid {
|
||||
let hits = server
|
||||
.store()
|
||||
.filter(
|
||||
.document_ids_matching(
|
||||
account_id,
|
||||
Collection::ContactCard,
|
||||
vec![Filter::eq(ContactField::Uid, uid.as_bytes().to_vec())],
|
||||
ContactField::Uid,
|
||||
uid.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !hits.results.is_empty() {
|
||||
if !hits.is_empty() {
|
||||
for document_id in resources
|
||||
.paths
|
||||
.iter()
|
||||
@@ -44,7 +43,7 @@ pub(super) async fn assert_is_unique_uid(
|
||||
})
|
||||
.map(|path| resources.resources[path.resource_idx].document_id)
|
||||
{
|
||||
if hits.results.contains(document_id) {
|
||||
if hits.contains(document_id) {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(JSContactProperty::Uid)
|
||||
.with_description(format!(
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::contact::{ContactCard, ContactCardComparator, ContactCardFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use nlp::tokenizers::word::WordTokenizer;
|
||||
use store::{SerializeInfallible, backend::MAX_TOKEN_LENGTH, query, roaring::RoaringBitmap};
|
||||
use store::{
|
||||
roaring::RoaringBitmap,
|
||||
search::{ContactSearchField, SearchComparator, SearchFilter},
|
||||
};
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
field::ContactField,
|
||||
};
|
||||
use utils::sanitize_email;
|
||||
|
||||
use crate::{ changes::state::JmapCacheState};
|
||||
|
||||
pub trait ContactCardQuery: Sync + Send {
|
||||
fn contact_card_query(
|
||||
&self,
|
||||
@@ -52,93 +52,188 @@ impl ContactCardQuery for Server {
|
||||
cache.children_ids(id.document_id()),
|
||||
)))
|
||||
}
|
||||
ContactCardFilter::Uid(uid) => {
|
||||
filters.push(SearchFilter::eq(ContactField::Uid, uid.into_bytes()))
|
||||
ContactCardFilter::Name(value)
|
||||
| ContactCardFilter::NameGiven(value)
|
||||
| ContactCardFilter::NameSurname(value)
|
||||
| ContactCardFilter::NameSurname2(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Name,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Nickname(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Nickname,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Organization(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Organization,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Phone(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Phone,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::OnlineService(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::OnlineService,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Address(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Address,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Note(value) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
ContactSearchField::Note,
|
||||
value,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::HasMember(value) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Member,
|
||||
value,
|
||||
));
|
||||
}
|
||||
ContactCardFilter::Kind(value) => {
|
||||
filters.push(SearchFilter::eq(ContactSearchField::Kind, value));
|
||||
}
|
||||
ContactCardFilter::Uid(value) => {
|
||||
filters.push(SearchFilter::eq(ContactSearchField::Uid, value))
|
||||
}
|
||||
ContactCardFilter::Email(email) => {
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Email,
|
||||
sanitize_email(&email).unwrap_or(email),
|
||||
))
|
||||
}
|
||||
ContactCardFilter::Email(email) => filters.push(SearchFilter::eq(
|
||||
ContactField::Email,
|
||||
sanitize_email(&email).unwrap_or(email).into_bytes(),
|
||||
)),
|
||||
ContactCardFilter::Text(value) => {
|
||||
for token in WordTokenizer::new(&value, MAX_TOKEN_LENGTH) {
|
||||
filters.push(SearchFilter::eq(
|
||||
ContactField::Text,
|
||||
token.word.into_owned().into_bytes(),
|
||||
));
|
||||
}
|
||||
filters.push(SearchFilter::Or);
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Name,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Nickname,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Organization,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Email,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Phone,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::OnlineService,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_unknown_text(
|
||||
ContactSearchField::Address,
|
||||
value.clone(),
|
||||
));
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
ContactSearchField::Note,
|
||||
value,
|
||||
self.core.jmap.default_language,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
ContactCardFilter::CreatedBefore(before) => filters.push(SearchFilter::lt(
|
||||
ContactField::Created,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
ContactSearchField::Created,
|
||||
before.timestamp(),
|
||||
)),
|
||||
ContactCardFilter::CreatedAfter(after) => filters.push(SearchFilter::gt(
|
||||
ContactField::Created,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
ContactSearchField::Created,
|
||||
after.timestamp(),
|
||||
)),
|
||||
ContactCardFilter::UpdatedBefore(before) => filters.push(SearchFilter::lt(
|
||||
ContactField::Updated,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
/*ContactCardFilter::UpdatedBefore(before) => filters.push(SearchFilter::lt(
|
||||
ContactSearchField::Updated,
|
||||
before.timestamp(),
|
||||
)),
|
||||
ContactCardFilter::UpdatedAfter(after) => filters.push(SearchFilter::gt(
|
||||
ContactField::Updated,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
)),
|
||||
ContactSearchField::Updated,
|
||||
after.timestamp(),
|
||||
)),*/
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_set = self
|
||||
.filter(account_id, Collection::ContactCard, filters)
|
||||
let comparators = request
|
||||
.sort
|
||||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|comparator| match comparator.property {
|
||||
ContactCardComparator::Created => Ok(SearchComparator::field(
|
||||
ContactSearchField::Created,
|
||||
comparator.is_ascending,
|
||||
)),
|
||||
/*ContactCardComparator::Updated => Ok(SearchComparator::field(
|
||||
ContactSearchField::Updated,
|
||||
comparator.is_ascending,
|
||||
)),*/
|
||||
other => Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(other.into_string())),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(account_id, Collection::ContactCard, filters, comparators)
|
||||
.await?;
|
||||
|
||||
if let Some(filter_mask) = filter_mask {
|
||||
result_set.apply_mask(filter_mask);
|
||||
}
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(ContactCardComparator::Updated)])
|
||||
for document_id in results {
|
||||
if filter_mask
|
||||
.as_ref()
|
||||
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
ContactCardComparator::Created => {
|
||||
SearchComparator::field(ContactField::Created, comparator.is_ascending)
|
||||
}
|
||||
ContactCardComparator::Updated => {
|
||||
SearchComparator::field(ContactField::Updated, comparator.is_ascending)
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{ changes::state::JmapCacheState};
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use groupware::cache::GroupwareCache;
|
||||
use jmap_proto::{
|
||||
@@ -12,7 +12,7 @@ use jmap_proto::{
|
||||
object::file_node::{FileNode, FileNodeFilter},
|
||||
request::MaybeInvalid,
|
||||
};
|
||||
use store::{query, roaring::RoaringBitmap};
|
||||
use store::{roaring::RoaringBitmap, search::SearchFilter};
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
@@ -122,63 +122,51 @@ impl FileNodeQuery for Server {
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_set = self
|
||||
.filter(account_id, Collection::FileNode, filters)
|
||||
.await?;
|
||||
|
||||
if let Some(filter_mask) = filter_mask {
|
||||
result_set.apply_mask(filter_mask);
|
||||
if request.sort.as_ref().is_some_and(|s| !s.is_empty()) {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details("Sorting is not supported on FileNode"));
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
)
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(account_id, Collection::FileNode, filters, vec![])
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
/*let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(FileNodeComparator::Updated)])
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
cache.get_state(false),
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if filter_mask
|
||||
.as_ref()
|
||||
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
FileNodeComparator::Created => {
|
||||
SearchComparator::field(ContactField::Created, comparator.is_ascending)
|
||||
}
|
||||
FileNodeComparator::Updated => {
|
||||
SearchComparator::field(ContactField::Updated, comparator.is_ascending)
|
||||
}
|
||||
unsupported => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details(unsupported.into_string()));
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
if request.sort.is_some_and(|s| !s.is_empty()) {
|
||||
return Err(trc::JmapEvent::UnsupportedSort
|
||||
.into_err()
|
||||
.details("Sorting is not supported on FileNode"));
|
||||
continue;
|
||||
}
|
||||
if !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, Default::default(), paginate, response)
|
||||
.await
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl MailboxQuery for Server {
|
||||
.items
|
||||
.iter()
|
||||
.filter(|mailbox| {
|
||||
!matches!(mailbox.role, SpecialUse::None) == has_role
|
||||
matches!(mailbox.role, SpecialUse::None) != has_role
|
||||
})
|
||||
.map(|m| m.document_id)
|
||||
.collect::<RoaringBitmap>(),
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::JmapMethods;
|
||||
use common::{Server, auth::AccessToken};
|
||||
use directory::{Permission, QueryParams, Type, backend::internal::manage::ManageDirectory};
|
||||
use http_proto::HttpSessionData;
|
||||
@@ -14,10 +13,12 @@ use jmap_proto::{
|
||||
types::state::State,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{query::ResultSet, roaring::RoaringBitmap};
|
||||
use store::{roaring::RoaringBitmap, search::SearchFilter};
|
||||
use trc::AddContext;
|
||||
use types::collection::Collection;
|
||||
|
||||
use crate::api::query::QueryResponseBuilder;
|
||||
|
||||
pub trait PrincipalQuery: Sync + Send {
|
||||
fn principal_query(
|
||||
&self,
|
||||
@@ -42,12 +43,6 @@ impl PrincipalQuery for Server {
|
||||
.details("The administrator has disabled directory queries.".to_string()));
|
||||
}
|
||||
|
||||
let mut result_set = ResultSet {
|
||||
account_id: request.account_id.document_id(),
|
||||
collection: Collection::Principal,
|
||||
results: RoaringBitmap::new(),
|
||||
};
|
||||
let mut is_set = true;
|
||||
let principal_ids = self
|
||||
.store()
|
||||
.list_principals(
|
||||
@@ -70,6 +65,7 @@ impl PrincipalQuery for Server {
|
||||
.map(|p| p.id())
|
||||
.collect::<RoaringBitmap>();
|
||||
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
@@ -81,74 +77,52 @@ impl PrincipalQuery for Server {
|
||||
.query(QueryParams::name(name.as_str()).with_return_member_of(false))
|
||||
.await?
|
||||
{
|
||||
if is_set || result_set.results.contains(principal.id()) {
|
||||
result_set.results =
|
||||
RoaringBitmap::from_sorted_iter([principal.id()]).unwrap();
|
||||
} else {
|
||||
result_set.results = RoaringBitmap::new();
|
||||
}
|
||||
} else {
|
||||
result_set.results = RoaringBitmap::new();
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(),
|
||||
));
|
||||
}
|
||||
is_set = false;
|
||||
}
|
||||
PrincipalFilter::Email(email) => {
|
||||
let mut ids = RoaringBitmap::new();
|
||||
if let Some(id) = self
|
||||
.email_to_id(self.directory(), &email, session.session_id)
|
||||
.await?
|
||||
{
|
||||
ids.insert(id);
|
||||
}
|
||||
if is_set {
|
||||
result_set.results = ids;
|
||||
is_set = false;
|
||||
} else {
|
||||
result_set.results &= ids;
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
RoaringBitmap::from_sorted_iter([id]).unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
PrincipalFilter::AccountIds(ids) => {
|
||||
let ids = ids
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
let id = id.document_id();
|
||||
if principal_ids.contains(id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<RoaringBitmap>();
|
||||
if is_set {
|
||||
result_set.results = ids;
|
||||
is_set = false;
|
||||
} else {
|
||||
result_set.results &= ids;
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| {
|
||||
let id = id.document_id();
|
||||
if principal_ids.contains(id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<RoaringBitmap>(),
|
||||
));
|
||||
}
|
||||
PrincipalFilter::Text(text) => {
|
||||
let ids = self
|
||||
.store()
|
||||
.list_principals(
|
||||
Some(text.as_str()),
|
||||
access_token.tenant.map(|t| t.id),
|
||||
&[],
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.id())
|
||||
.collect::<RoaringBitmap>();
|
||||
|
||||
if is_set {
|
||||
result_set.results = ids;
|
||||
is_set = false;
|
||||
} else {
|
||||
result_set.results &= ids;
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
self.store()
|
||||
.list_principals(
|
||||
Some(text.as_str()),
|
||||
access_token.tenant.map(|t| t.id),
|
||||
&[],
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.id())
|
||||
.collect::<RoaringBitmap>(),
|
||||
));
|
||||
}
|
||||
PrincipalFilter::Type(principal_type) => {
|
||||
let typ = match principal_type {
|
||||
@@ -159,28 +133,22 @@ impl PrincipalQuery for Server {
|
||||
PrincipalType::Other => Type::Other,
|
||||
};
|
||||
|
||||
let ids = self
|
||||
.store()
|
||||
.list_principals(
|
||||
None,
|
||||
access_token.tenant.map(|t| t.id),
|
||||
&[typ],
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.id())
|
||||
.collect::<RoaringBitmap>();
|
||||
|
||||
if is_set {
|
||||
result_set.results = ids;
|
||||
is_set = false;
|
||||
} else {
|
||||
result_set.results &= ids;
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
self.store()
|
||||
.list_principals(
|
||||
None,
|
||||
access_token.tenant.map(|t| t.id),
|
||||
&[typ],
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|p| p.id())
|
||||
.collect::<RoaringBitmap>(),
|
||||
));
|
||||
}
|
||||
other => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
@@ -188,28 +156,39 @@ impl PrincipalQuery for Server {
|
||||
.details(other.to_string()));
|
||||
}
|
||||
},
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter
|
||||
.into_err()
|
||||
.details("Logical operators are not supported"));
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_set {
|
||||
result_set.results = principal_ids;
|
||||
} else {
|
||||
result_set.results &= principal_ids;
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(result_set.results.len() as usize, State::Initial, &request)
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(u32::MAX, Collection::Principal, filters, vec![])
|
||||
.await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
self.sort(result_set, Vec::new(), paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&request,
|
||||
);
|
||||
|
||||
for document_id in results {
|
||||
if principal_ids.contains(document_id) && !response.add(0, document_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,15 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{ UpdateResults};
|
||||
use crate::api::query::QueryResponseBuilder;
|
||||
use common::{Server, sharing::notification::ShareNotification};
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::share_notification::{self, ShareNotificationFilter},
|
||||
types::state::State,
|
||||
};
|
||||
use store::{
|
||||
Deserialize, IterateParams, LogKey, U64_LEN, query::ResultSet, write::key::DeserializeBigEndian,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use store::{Deserialize, IterateParams, LogKey, U64_LEN, write::key::DeserializeBigEndian};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
@@ -79,12 +76,6 @@ impl ShareNotificationQuery for Server {
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut result_set = ResultSet {
|
||||
account_id,
|
||||
collection: Collection::None,
|
||||
results: Default::default(),
|
||||
};
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
@@ -113,7 +104,6 @@ impl ShareNotificationQuery for Server {
|
||||
}
|
||||
}
|
||||
|
||||
result_set.results.insert(results.len() as u32);
|
||||
results.push(Id::from(change_id));
|
||||
|
||||
Ok(true)
|
||||
@@ -122,20 +112,19 @@ impl ShareNotificationQuery for Server {
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let (mut response, paginate) = self
|
||||
.build_query_response(result_set.results.len() as usize, State::Initial, &request)
|
||||
.await?;
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
State::Initial,
|
||||
&request,
|
||||
);
|
||||
|
||||
if let Some(mut paginate) = paginate {
|
||||
for result in results {
|
||||
if !paginate.add_id(result) {
|
||||
break;
|
||||
}
|
||||
for id in results {
|
||||
if !response.add_id(id) {
|
||||
break;
|
||||
}
|
||||
|
||||
response.update_results(paginate.build())?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::StateManager;
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
|
||||
use common::Server;
|
||||
use email::sieve::ingest::SieveScriptIngest;
|
||||
use jmap_proto::{
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::sieve::{Sieve, SieveComparator, SieveFilter},
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
query::{self},
|
||||
roaring::RoaringBitmap,
|
||||
IndexKeyPrefix, IterateParams, U32_LEN, ahash::AHashSet, roaring::RoaringBitmap,
|
||||
search::SearchFilter, write::key::DeserializeBigEndian,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::SieveField,
|
||||
@@ -48,73 +49,149 @@ impl SieveScriptQuery for Server {
|
||||
None
|
||||
};
|
||||
|
||||
let mut document_ids = RoaringBitmap::new();
|
||||
let mut names = Vec::new();
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
IndexKeyPrefix {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
field: SieveField::Name.into(),
|
||||
},
|
||||
IndexKeyPrefix {
|
||||
account_id,
|
||||
collection: Collection::SieveScript.into(),
|
||||
field: u8::from(Collection::SieveScript) + 1,
|
||||
},
|
||||
)
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
|
||||
|
||||
names.push((
|
||||
document_id,
|
||||
key.get(IndexKeyPrefix::len()..key.len() - U32_LEN)
|
||||
.and_then(|v| std::str::from_utf8(v).ok())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
));
|
||||
|
||||
document_ids.insert(document_id);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
SieveFilter::Name(name) => {
|
||||
filters.push(SearchFilter::contains(SieveField::Name, &name))
|
||||
let name = name.to_lowercase();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
names
|
||||
.iter()
|
||||
.filter_map(|(id, n)| (n.contains(&name)).then_some(*id))
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
}
|
||||
SieveFilter::IsActive(is_active) => {
|
||||
if !is_active {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
active_script_id,
|
||||
)));
|
||||
if !is_active {
|
||||
filters.push(SearchFilter::End);
|
||||
let active_script_id = active_script_id.unwrap();
|
||||
|
||||
if is_active {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter([
|
||||
active_script_id,
|
||||
])));
|
||||
} else {
|
||||
let mut inactive_set = document_ids.clone();
|
||||
inactive_set.remove(active_script_id);
|
||||
filters.push(SearchFilter::is_in_set(inactive_set));
|
||||
}
|
||||
}
|
||||
SieveFilter::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_set = self
|
||||
.filter(account_id, Collection::SieveScript, filters)
|
||||
.await?;
|
||||
// Parse sort criteria
|
||||
let mut sort_by_active = None;
|
||||
for comparator in request
|
||||
.sort
|
||||
.take()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
match comparator.property {
|
||||
SieveComparator::Name => {
|
||||
if !comparator.is_ascending {
|
||||
names.reverse();
|
||||
}
|
||||
}
|
||||
SieveComparator::IsActive => {
|
||||
sort_by_active = Some(comparator.is_ascending);
|
||||
}
|
||||
SieveComparator::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
self.get_state(account_id, SyncCollection::SieveScript)
|
||||
.await?,
|
||||
&request,
|
||||
)
|
||||
.await?;
|
||||
let mut results = self
|
||||
.search_store()
|
||||
.query(account_id, Collection::SieveScript, filters, vec![])
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(SieveComparator::Name)])
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
self.get_state(account_id, SyncCollection::SieveScript)
|
||||
.await?,
|
||||
&request,
|
||||
);
|
||||
|
||||
if !results.is_empty() {
|
||||
if matches!(sort_by_active, Some(true))
|
||||
&& results.remove(&active_script_id.unwrap_or_default())
|
||||
&& !response.add(0, active_script_id.unwrap())
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
SieveComparator::Name => {
|
||||
SearchComparator::field(SieveField::Name, comparator.is_ascending)
|
||||
}
|
||||
SieveComparator::IsActive => SearchComparator::set(
|
||||
RoaringBitmap::from_iter(active_script_id),
|
||||
comparator.is_ascending,
|
||||
),
|
||||
SieveComparator::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
|
||||
}
|
||||
});
|
||||
return response.build();
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
let mut last_id = None;
|
||||
for (document_id, _) in names {
|
||||
if results.contains(&document_id) {
|
||||
if sort_by_active.is_some() && Some(document_id) == active_script_id {
|
||||
last_id = Some(document_id);
|
||||
} else if !response.add(0, document_id) {
|
||||
return response.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_id) = last_id {
|
||||
response.add(0, active_id);
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,13 +404,13 @@ impl SieveScriptSet for Server {
|
||||
.as_ref()
|
||||
.is_none_or(|(_, obj)| obj.inner.name != value.as_ref())
|
||||
&& let Some(id) = self
|
||||
.filter(
|
||||
.document_ids_matching(
|
||||
ctx.resource_token.account_id,
|
||||
Collection::SieveScript,
|
||||
vec![Filter::eq(SieveField::Name, value.as_bytes().to_vec())],
|
||||
SieveField::Name,
|
||||
value.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
.results
|
||||
.min()
|
||||
{
|
||||
return Ok(Err(SetError::already_exists()
|
||||
|
||||
@@ -19,7 +19,11 @@ use jmap_tools::{Key, Map, Value};
|
||||
use smtp::queue::{ArchivedError, ArchivedErrorDetails, ArchivedStatus, Message, spool::SmtpSpool};
|
||||
use smtp_proto::ArchivedResponse;
|
||||
use std::future::Future;
|
||||
use store::rkyv::option::ArchivedOption;
|
||||
use store::{
|
||||
IterateParams, U32_LEN, ValueKey,
|
||||
rkyv::option::ArchivedOption,
|
||||
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
@@ -54,21 +58,45 @@ impl EmailSubmissionGet for Server {
|
||||
EmailSubmissionProperty::MdnBlobIds,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let email_submission_ids = self
|
||||
.document_ids(
|
||||
account_id,
|
||||
Collection::EmailSubmission,
|
||||
EmailSubmissionField::EmailId,
|
||||
)
|
||||
.await?;
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
email_submission_ids
|
||||
.iter()
|
||||
.take(self.core.jmap.get_max_objects)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
let mut ids = Vec::with_capacity(16);
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: now() - (3 * 86400),
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending()
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
ids.push(Id::from(key.deserialize_be_u32(key.len() - U32_LEN)?));
|
||||
|
||||
Ok(ids.len() < self.core.jmap.get_max_objects)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
ids
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
@@ -83,10 +111,6 @@ impl EmailSubmissionGet for Server {
|
||||
for id in ids {
|
||||
// Obtain the email_submission object
|
||||
let document_id = id.document_id();
|
||||
if !email_submission_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
continue;
|
||||
}
|
||||
let submission_ = if let Some(submission) = self
|
||||
.archive(account_id, Collection::EmailSubmission, document_id)
|
||||
.await?
|
||||
|
||||
@@ -4,19 +4,23 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::StateManager;
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
|
||||
use common::Server;
|
||||
use email::submission::UndoStatus;
|
||||
use jmap_proto::{
|
||||
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::email_submission::{self, EmailSubmissionComparator, EmailSubmissionFilter},
|
||||
request::IntoValid,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
SerializeInfallible,
|
||||
query::{self},
|
||||
IterateParams, U32_LEN, U64_LEN, ValueKey,
|
||||
ahash::AHashSet,
|
||||
roaring::RoaringBitmap,
|
||||
search::SearchFilter,
|
||||
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::EmailSubmissionField,
|
||||
@@ -29,123 +33,213 @@ pub trait EmailSubmissionQuery: Sync + Send {
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
struct Submission {
|
||||
document_id: u32,
|
||||
send_at: u64,
|
||||
email_id: u32,
|
||||
thread_id: u32,
|
||||
identity_id: u32,
|
||||
undo_status: u8,
|
||||
}
|
||||
|
||||
impl EmailSubmissionQuery for Server {
|
||||
async fn email_submission_query(
|
||||
&self,
|
||||
mut request: QueryRequest<email_submission::EmailSubmission>,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
|
||||
let mut submissions = Vec::with_capacity(16);
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: now() - (3 * 86400),
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::CalendarEventNotification.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
submissions.push(Submission {
|
||||
document_id: key.deserialize_be_u32(key.len() - U32_LEN)?,
|
||||
send_at: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
|
||||
email_id: value.deserialize_be_u32(0)?,
|
||||
thread_id: value.deserialize_be_u32(U32_LEN)?,
|
||||
identity_id: value.deserialize_be_u32(U32_LEN + U32_LEN)?,
|
||||
undo_status: value.last().copied().unwrap(),
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
EmailSubmissionFilter::IdentityIds(ids) => {
|
||||
filters.push(SearchFilter::Or);
|
||||
for id in ids.into_valid() {
|
||||
filters.push(SearchFilter::eq(
|
||||
EmailSubmissionField::IdentityId,
|
||||
id.document_id().serialize(),
|
||||
));
|
||||
}
|
||||
filters.push(SearchFilter::End);
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.identity_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::EmailIds(ids) => {
|
||||
filters.push(SearchFilter::Or);
|
||||
for id in ids.into_valid() {
|
||||
filters.push(SearchFilter::eq(
|
||||
EmailSubmissionField::EmailId,
|
||||
id.id().serialize(),
|
||||
));
|
||||
}
|
||||
filters.push(SearchFilter::End);
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.email_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::ThreadIds(ids) => {
|
||||
filters.push(SearchFilter::Or);
|
||||
for id in ids.into_valid() {
|
||||
filters.push(SearchFilter::eq(
|
||||
EmailSubmissionField::ThreadId,
|
||||
id.document_id().serialize(),
|
||||
));
|
||||
}
|
||||
filters.push(SearchFilter::End);
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.thread_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::UndoStatus(undo_status) => {
|
||||
filters.push(SearchFilter::eq(
|
||||
EmailSubmissionField::UndoStatus,
|
||||
match undo_status {
|
||||
email_submission::UndoStatus::Pending => UndoStatus::Pending,
|
||||
email_submission::UndoStatus::Final => UndoStatus::Final,
|
||||
email_submission::UndoStatus::Canceled => UndoStatus::Canceled,
|
||||
}
|
||||
.as_index()
|
||||
.serialize(),
|
||||
))
|
||||
let undo_status = match undo_status {
|
||||
email_submission::UndoStatus::Pending => UndoStatus::Pending,
|
||||
email_submission::UndoStatus::Final => UndoStatus::Final,
|
||||
email_submission::UndoStatus::Canceled => UndoStatus::Canceled,
|
||||
}
|
||||
.as_index();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.undo_status == undo_status)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::Before(before) => {
|
||||
let before = before.timestamp() as u64;
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.send_at < before)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::After(after) => {
|
||||
let after = after.timestamp() as u64;
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.send_at > after)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::Before(before) => filters.push(SearchFilter::lt(
|
||||
EmailSubmissionField::SendAt,
|
||||
(before.timestamp() as u64).serialize(),
|
||||
)),
|
||||
EmailSubmissionFilter::After(after) => filters.push(SearchFilter::gt(
|
||||
EmailSubmissionField::SendAt,
|
||||
(after.timestamp() as u64).serialize(),
|
||||
)),
|
||||
|
||||
EmailSubmissionFilter::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
|
||||
}
|
||||
},
|
||||
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_set = self
|
||||
.filter(account_id, Collection::EmailSubmission, filters)
|
||||
.await?;
|
||||
let results = self
|
||||
.search_store()
|
||||
.query(account_id, Collection::ContactCard, filters, vec![])
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
let (response, paginate) = self
|
||||
.build_query_response(
|
||||
result_set.results.len() as usize,
|
||||
self.get_state(account_id, SyncCollection::EmailSubmission)
|
||||
.await?,
|
||||
&request,
|
||||
)
|
||||
.await?;
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len(),
|
||||
self.core.jmap.query_max_results,
|
||||
self.get_state(account_id, SyncCollection::EmailSubmission)
|
||||
.await?,
|
||||
&request,
|
||||
);
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| vec![Comparator::descending(EmailSubmissionComparator::SentAt)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
EmailSubmissionComparator::EmailId => SearchComparator::field(
|
||||
EmailSubmissionField::EmailId,
|
||||
comparator.is_ascending,
|
||||
),
|
||||
EmailSubmissionComparator::ThreadId => SearchComparator::field(
|
||||
EmailSubmissionField::ThreadId,
|
||||
comparator.is_ascending,
|
||||
),
|
||||
EmailSubmissionComparator::SentAt => SearchComparator::field(
|
||||
EmailSubmissionField::SendAt,
|
||||
comparator.is_ascending,
|
||||
),
|
||||
if !results.is_empty() {
|
||||
if let Some(comparator) = request.sort.take().unwrap_or_default().into_iter().next() {
|
||||
match comparator.property {
|
||||
EmailSubmissionComparator::EmailId => {
|
||||
if comparator.is_ascending {
|
||||
submissions.sort_by_key(|s| s.email_id);
|
||||
} else {
|
||||
submissions.sort_by_key(|s| u32::MAX - s.email_id);
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::ThreadId => {
|
||||
if comparator.is_ascending {
|
||||
submissions.sort_by_key(|s| s.thread_id);
|
||||
} else {
|
||||
submissions.sort_by_key(|s| u32::MAX - s.thread_id);
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::SentAt => {
|
||||
if !comparator.is_ascending {
|
||||
submissions.reverse();
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
for submission in submissions {
|
||||
if results.contains(&submission.document_id)
|
||||
&& !response.add(0, submission.document_id)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,9 @@ use jmap_proto::{
|
||||
};
|
||||
use jmap_tools::Map;
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
ahash::AHashMap,
|
||||
query::{Comparator, ResultSet, sort::Pagination},
|
||||
roaring::RoaringBitmap,
|
||||
};
|
||||
use store::{ahash::AHashMap, roaring::RoaringBitmap};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::EmailField,
|
||||
id::Id,
|
||||
};
|
||||
use types::{collection::SyncCollection, id::Id};
|
||||
|
||||
pub trait ThreadGet: Sync + Send {
|
||||
fn thread_get(
|
||||
@@ -83,23 +75,11 @@ impl ThreadGet for Server {
|
||||
let mut thread: Map<'_, ThreadProperty, ThreadValue> =
|
||||
Map::with_capacity(2).with_key_value(ThreadProperty::Id, id);
|
||||
if add_email_ids {
|
||||
let doc_count = document_ids.len() as usize;
|
||||
let todo = " sorted as vec![Comparator::ascending(EmailField::ReceivedAt)],";
|
||||
thread.insert_unchecked(
|
||||
ThreadProperty::EmailIds,
|
||||
self.core
|
||||
.storage
|
||||
.data
|
||||
.sort(
|
||||
ResultSet::new(account_id, Collection::Email, document_ids),
|
||||
vec![],
|
||||
Pagination::new(doc_count, 0, None, 0),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.ids
|
||||
document_ids
|
||||
.into_iter()
|
||||
.map(|id| Id::from_parts(thread_id, id.document_id()))
|
||||
.map(|id| Id::from_parts(thread_id, id))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{ changes::state::StateManager};
|
||||
use crate::changes::state::StateManager;
|
||||
use common::Server;
|
||||
use email::sieve::{SieveScript, ingest::SieveScriptIngest};
|
||||
use jmap_proto::{
|
||||
@@ -17,7 +17,6 @@ use jmap_proto::{
|
||||
};
|
||||
use jmap_tools::{Map, Value};
|
||||
use std::future::Future;
|
||||
use store::SearchFilter;
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
@@ -162,12 +161,13 @@ impl VacationResponseGet for Server {
|
||||
}
|
||||
|
||||
async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> {
|
||||
self.filter(
|
||||
self.document_ids_matching(
|
||||
account_id,
|
||||
Collection::SieveScript,
|
||||
vec![Filter::eq(SieveField::Name, "vacation".as_bytes().to_vec())],
|
||||
SieveField::Name,
|
||||
"vacation".as_bytes(),
|
||||
)
|
||||
.await
|
||||
.map(|r| r.results.min())
|
||||
.map(|r| r.min())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user