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}