Skip to main content

jmap_server/
backend.rs

1//! Shared backend infrastructure for all JMAP server crates.
2//!
3//! Re-exports the marker traits from `jmap-types` and adds the result types,
4//! `BackendChangesError`, and [`JmapBackend`] supertrait. Domain crates add
5//! their write-side methods and domain-specific error variants on top.
6
7pub use jmap_types::{GetObject, JmapObject, QueryObject, SetObject};
8
9// ---------------------------------------------------------------------------
10// Backend error envelopes
11// ---------------------------------------------------------------------------
12
13/// Error type returned by [`JmapBackend::get_changes`] and
14/// [`JmapBackend::query_changes`].
15#[non_exhaustive]
16#[derive(Debug)]
17pub enum BackendChangesError<E> {
18    /// The `sinceState` is too old or the server cannot calculate the full set
19    /// of intermediate states. Maps to `tooManyChanges` in the response with
20    /// the given suggested limit. Use `limit: 0` for `cannotCalculateChanges`.
21    TooManyChanges { limit: u64 },
22    /// An unexpected storage-layer error.
23    Other(E),
24}
25
26impl<E: std::fmt::Display> std::fmt::Display for BackendChangesError<E> {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::TooManyChanges { limit: 0 } => write!(f, "cannot calculate changes"),
30            Self::TooManyChanges { limit } => write!(f, "too many changes (limit: {limit})"),
31            Self::Other(e) => write!(f, "{e}"),
32        }
33    }
34}
35
36impl<E: std::error::Error + 'static> std::error::Error for BackendChangesError<E> {
37    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
38        match self {
39            Self::Other(e) => Some(e),
40            _ => None,
41        }
42    }
43}
44
45impl<E> From<E> for BackendChangesError<E> {
46    fn from(e: E) -> Self {
47        Self::Other(e)
48    }
49}
50
51impl<E: std::error::Error> From<BackendChangesError<E>> for jmap_types::JmapError {
52    fn from(e: BackendChangesError<E>) -> Self {
53        match e {
54            BackendChangesError::TooManyChanges { limit: 0 } => {
55                jmap_types::JmapError::cannot_calculate_changes()
56            }
57            BackendChangesError::TooManyChanges { limit } => {
58                jmap_types::JmapError::too_many_changes_with_limit(limit)
59            }
60            BackendChangesError::Other(inner) => {
61                jmap_types::JmapError::server_fail(inner.to_string())
62            }
63        }
64    }
65}
66
67// ---------------------------------------------------------------------------
68// Result types
69// ---------------------------------------------------------------------------
70
71/// Result of a `/changes` call (RFC 8620 §5.2).
72#[derive(Debug)]
73#[non_exhaustive]
74pub struct ChangesResult {
75    /// Ids of objects that were created since `sinceState`.
76    pub created: Vec<jmap_types::Id>,
77    /// Ids of objects that were updated since `sinceState`.
78    pub updated: Vec<jmap_types::Id>,
79    /// Ids of objects that were destroyed since `sinceState`.
80    pub destroyed: Vec<jmap_types::Id>,
81    /// `true` if there are more changes beyond this batch.
82    pub has_more_changes: bool,
83    /// The current state token after applying all reported changes.
84    pub new_state: jmap_types::State,
85}
86
87impl ChangesResult {
88    /// Construct a [`ChangesResult`].
89    pub fn new(
90        created: Vec<jmap_types::Id>,
91        updated: Vec<jmap_types::Id>,
92        destroyed: Vec<jmap_types::Id>,
93        has_more_changes: bool,
94        new_state: jmap_types::State,
95    ) -> Self {
96        Self {
97            created,
98            updated,
99            destroyed,
100            has_more_changes,
101            new_state,
102        }
103    }
104}
105
106/// Result of a `/query` call (RFC 8620 §5.5).
107#[derive(Debug)]
108#[non_exhaustive]
109pub struct QueryResult {
110    /// The ordered list of matching object ids.
111    pub ids: Vec<jmap_types::Id>,
112    /// The 0-based index of the first returned id in the complete result list.
113    pub position: i64,
114    /// Total number of results, if the backend can calculate it.
115    pub total: Option<u64>,
116    /// Opaque query state token for subsequent `/queryChanges` calls.
117    pub query_state: jmap_types::State,
118    /// Whether the backend supports `/queryChanges` for this query.
119    pub can_calculate_changes: bool,
120}
121
122impl QueryResult {
123    /// Construct a [`QueryResult`].
124    pub fn new(
125        ids: Vec<jmap_types::Id>,
126        position: i64,
127        total: Option<u64>,
128        query_state: jmap_types::State,
129        can_calculate_changes: bool,
130    ) -> Self {
131        Self {
132            ids,
133            position,
134            total,
135            query_state,
136            can_calculate_changes,
137        }
138    }
139}
140
141/// One entry in the `added` list of a `/queryChanges` response (RFC 8620 §5.6).
142#[derive(Debug)]
143#[non_exhaustive]
144pub struct AddedItem {
145    /// The id of the newly-added object.
146    pub id: jmap_types::Id,
147    /// Its 0-based position in the result list after applying all changes.
148    pub index: u64,
149}
150
151impl AddedItem {
152    /// Construct an [`AddedItem`].
153    pub fn new(id: jmap_types::Id, index: u64) -> Self {
154        Self { id, index }
155    }
156}
157
158/// Result of a `/queryChanges` call (RFC 8620 §5.6).
159#[derive(Debug)]
160#[non_exhaustive]
161pub struct QueryChangesResult {
162    /// The query state token supplied by the client in `sinceQueryState`.
163    pub old_query_state: jmap_types::State,
164    /// The current query state token.
165    pub new_query_state: jmap_types::State,
166    /// Total number of results in the new query, if the backend can calculate it.
167    pub total: Option<u64>,
168    /// Ids removed from the result set since `oldQueryState`.
169    pub removed: Vec<jmap_types::Id>,
170    /// Ids added to the result set since `oldQueryState`, with their positions.
171    pub added: Vec<AddedItem>,
172}
173
174impl QueryChangesResult {
175    /// Construct a [`QueryChangesResult`].
176    pub fn new(
177        old_query_state: jmap_types::State,
178        new_query_state: jmap_types::State,
179        total: Option<u64>,
180        removed: Vec<jmap_types::Id>,
181        added: Vec<AddedItem>,
182    ) -> Self {
183        Self {
184            old_query_state,
185            new_query_state,
186            total,
187            removed,
188            added,
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// JmapBackend — the read-side supertrait
195// ---------------------------------------------------------------------------
196
197/// Read-side backend supertrait shared by all JMAP server crates.
198///
199/// Domain-specific backend traits (`MailBackend`, `ChatBackend`, etc.) require
200/// this trait as a supertrait and add write-side methods on top.
201///
202/// Only the read operations that have an identical signature across all JMAP
203/// object types belong here. Write operations (`create_object`, `update_object`,
204/// `destroy_object`) and domain-specific operations remain in the domain crate.
205///
206/// The `collapse_threads` parameter on `query_changes` is included for
207/// `Email/queryChanges` (RFC 8621 §4.5). Non-mail backends should pass `false`
208/// and may ignore the parameter.
209///
210/// This trait is not object-safe by design (generic methods). Use
211/// `Arc<impl JmapBackend>` when sharing across tasks.
212pub trait JmapBackend: Send + Sync + 'static {
213    /// The error type returned by storage operations.
214    type Error: std::error::Error + Send + Sync + 'static;
215
216    /// Fetch objects by id (or all objects when `ids` is `None`).
217    ///
218    /// Returns `(found, not_found)` — objects that exist and ids that do not.
219    fn get_objects<O: GetObject + Send + Sync>(
220        &self,
221        account_id: &jmap_types::Id,
222        ids: Option<&[jmap_types::Id]>,
223        properties: Option<&[<O as JmapObject>::Property]>,
224    ) -> impl std::future::Future<Output = Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error>> + Send;
225
226    /// Return the current state token for an object type in the given account.
227    fn get_state<O: JmapObject + Send + Sync>(
228        &self,
229        account_id: &jmap_types::Id,
230    ) -> impl std::future::Future<Output = Result<jmap_types::State, Self::Error>> + Send;
231
232    /// Return changes since `since_state`, up to `max_changes` entries.
233    fn get_changes<O: JmapObject + Send + Sync>(
234        &self,
235        account_id: &jmap_types::Id,
236        since_state: &jmap_types::State,
237        max_changes: Option<u64>,
238    ) -> impl std::future::Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send;
239
240    /// Execute a `/query` and return a page of matching ids.
241    ///
242    /// `position` may be negative — negative values are relative to the end of
243    /// the result set per RFC 8620 §5.5 (e.g. -1 means the last result).
244    fn query_objects<O: QueryObject + Send + Sync>(
245        &self,
246        account_id: &jmap_types::Id,
247        filter: Option<&O::Filter>,
248        sort: Option<&[O::Comparator]>,
249        limit: Option<u64>,
250        position: i64,
251    ) -> impl std::future::Future<Output = Result<QueryResult, Self::Error>> + Send;
252
253    /// Execute a `/queryChanges` and return deltas since `since_query_state`.
254    ///
255    /// `collapse_threads` is only meaningful for `Email/queryChanges`
256    /// (RFC 8621 §4.5). Pass `false` for all other object types.
257    #[allow(clippy::too_many_arguments)]
258    fn query_changes<O: QueryObject + Send + Sync>(
259        &self,
260        account_id: &jmap_types::Id,
261        since_query_state: &jmap_types::State,
262        filter: Option<&O::Filter>,
263        sort: Option<&[O::Comparator]>,
264        max_changes: Option<u64>,
265        up_to_id: Option<&jmap_types::Id>,
266        collapse_threads: bool,
267    ) -> impl std::future::Future<
268        Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>,
269    > + Send;
270}
271
272// ---------------------------------------------------------------------------
273// Tests
274// ---------------------------------------------------------------------------
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    /// Oracle: BackendChangesError::TooManyChanges { limit: 0 } must map to
281    /// cannotCalculateChanges (RFC 8620 §5.6), not tooManyChanges with limit 0.
282    ///
283    /// limit=0 is the convention for "cannot calculate".
284    #[test]
285    fn backend_changes_error_limit_zero_maps_to_cannot_calculate() {
286        let err = jmap_types::JmapError::from(
287            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 0 },
288        );
289        assert_eq!(
290            err.error_type.as_str(),
291            "cannotCalculateChanges",
292            "limit=0 must produce cannotCalculateChanges; got: {:?}",
293            err.error_type
294        );
295    }
296
297    /// Oracle: BackendChangesError::TooManyChanges { limit: N } (N > 0) maps to
298    /// tooManyChanges with the suggested limit.
299    #[test]
300    fn backend_changes_error_nonzero_limit_maps_to_too_many_changes() {
301        let err = jmap_types::JmapError::from(
302            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 50 },
303        );
304        assert_eq!(
305            err.error_type.as_str(),
306            "tooManyChanges",
307            "limit=50 must produce tooManyChanges; got: {:?}",
308            err.error_type
309        );
310    }
311}