Skip to main content

paperless_api/
client.rs

1//! The central client for interacting with Paperless.
2
3use std::{borrow::Cow, collections::HashMap, path::Path, str::FromStr, sync::Arc};
4
5use enum_iterator::Sequence;
6use reqwest::{
7    Method, StatusCode,
8    header::{ACCEPT, HeaderMap, HeaderName, InvalidHeaderValue},
9    multipart,
10};
11use serde::{Deserialize, Serialize, de::DeserializeOwned};
12use tracing::{debug, trace};
13
14use crate::{
15    Error, Group, Result, SavedView, User,
16    document::{Document, DocumentData},
17    document_query::DocumentQueryBuilder,
18    dto::{CreateDto, Item, UpdateDto},
19    id::{
20        CorrespondentId, CustomFieldId, DocumentId, DocumentTypeId, GroupId, ItemId, StoragePathId,
21        TagId, TaskId, UserId,
22    },
23    metadata::{
24        correspondent::Correspondent, custom_field::CustomField, document_type::DocumentType,
25        storage_path::StoragePath, tag::Tag,
26    },
27    task::Task,
28    util,
29    workflow::Workflow,
30};
31
32/// Selects which cached metadata to refresh.
33///
34/// Cached data is data which is rarely updated;
35/// refreshing it is normally not necessary on every request.
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Sequence)]
37#[non_exhaustive]
38pub enum RefreshMetaData {
39    Tags,
40    CustomFields,
41    Correspondents,
42    DocumentTypes,
43    Groups,
44    Users,
45    StoragePaths,
46}
47
48/// Client to interact with Paperless.
49#[derive(Debug, Clone)]
50pub struct PaperlessClient {
51    /// Whether to request full permissions data for items.
52    pub request_full_permissions: bool,
53
54    /// Whether to always request the full document content.
55    pub request_full_content: bool,
56
57    pub(crate) base_url: Arc<str>,
58
59    client: reqwest::Client,
60    cached_data: Arc<CachedData>,
61}
62
63/// Search hit data
64#[derive(Debug, Clone, Deserialize)]
65pub struct SearchHit {
66    /// The score of the search hit.
67    pub score: f32,
68
69    /// Highlight of the search hit in the document content.
70    pub highlights: Option<String>,
71
72    /// Highlight of the search hit in the note content.
73    pub note_highlights: Option<String>,
74
75    /// Rank of the search hit.
76    pub rank: u32,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80struct SearchResult {
81    #[serde(flatten)]
82    document_data: DocumentData,
83
84    #[serde(rename = "__search_hit__")]
85    search_hit: SearchHit,
86}
87
88#[derive(Debug, Clone)]
89struct CachedData {
90    correspondents: HashMap<CorrespondentId, Correspondent>,
91    custom_fields: HashMap<CustomFieldId, CustomField>,
92    document_types: HashMap<DocumentTypeId, DocumentType>,
93    groups: HashMap<GroupId, Group>,
94    storage_paths: HashMap<StoragePathId, StoragePath>,
95    tags: HashMap<TagId, Tag>,
96    users: HashMap<UserId, User>,
97}
98
99#[derive(Debug, Deserialize)]
100struct PaginatedResponse<T> {
101    results: Vec<T>,
102    next: Option<String>,
103}
104
105impl PaperlessClient {
106    /// Create a new Paperless client.
107    ///
108    /// # Arguments
109    ///
110    /// * `base_url` - The base URL of the Paperless API.
111    /// * `token` - The authentication token for the Paperless API.
112    /// * `headers` - Optional additional headers to include in requests.
113    pub fn new(
114        base_url: &str,
115        token: &str,
116        headers: Option<&HashMap<String, String>>,
117    ) -> std::result::Result<Self, String> {
118        Self::new_with_client(
119            base_url,
120            token,
121            headers,
122            reqwest::Client::builder().zstd(true),
123        )
124    }
125
126    /// Create a new Paperless client.
127    ///
128    /// Provide a [`reqwest::ClientBuilder`] to customize the HTTP client,
129    /// such as adding custom headers or disabling compression.
130    ///
131    /// # Arguments
132    ///
133    /// * `base_url` - The base URL of the Paperless API.
134    /// * `token` - The authentication token for the Paperless API.
135    /// * `headers` - Optional additional headers to include in requests.
136    /// * `client_builder` - [`reqwest::ClientBuilder`] to use for creating the HTTP client.
137    pub fn new_with_client(
138        base_url: &str,
139        token: &str,
140        headers: Option<&HashMap<String, String>>,
141        client_builder: reqwest::ClientBuilder,
142    ) -> std::result::Result<Self, String> {
143        let mut headers_map = HeaderMap::new();
144
145        // Add additional headers if provided
146        if let Some(headers) = headers {
147            for (key, value) in headers {
148                headers_map.insert(
149                    HeaderName::from_str(key).map_err(|err| err.to_string())?,
150                    value
151                        .parse()
152                        .map_err(|err: InvalidHeaderValue| err.to_string())?,
153                );
154            }
155        }
156
157        // Add the Paperless token header
158        headers_map.insert(
159            HeaderName::from_str("Authorization").map_err(|err| err.to_string())?,
160            format!("Token {token}")
161                .parse()
162                .map_err(|err: InvalidHeaderValue| err.to_string())?,
163        );
164
165        Ok(Self {
166            request_full_permissions: false,
167            request_full_content: false,
168            base_url: base_url.into(),
169            client: client_builder
170                .default_headers(headers_map)
171                .build()
172                .map_err(|err| err.to_string())?,
173            cached_data: Arc::new(CachedData {
174                custom_fields: HashMap::new(),
175                correspondents: HashMap::new(),
176                document_types: HashMap::new(),
177                groups: HashMap::new(),
178                storage_paths: HashMap::new(),
179                tags: HashMap::new(),
180                users: HashMap::new(),
181            }),
182        })
183    }
184
185    /// Sets whether to request full permissions data for items during refresh.
186    ///
187    /// If not enabled only simple permission data is loaded.
188    /// See [`ItemPermissions`](crate::metadata::permission::ItemPermissions) for more details.
189    #[must_use]
190    pub fn with_full_permissions(mut self, req: bool) -> Self {
191        self.request_full_permissions = req;
192        self
193    }
194
195    #[must_use]
196    pub fn with_full_content(mut self, full_content: bool) -> Self {
197        self.request_full_content = full_content;
198        self
199    }
200
201    /// Loads all items of the given item type from the API.
202    pub async fn load_items<T: Item + DeserializeOwned>(&self) -> Result<HashMap<T::Id, T>> {
203        let endpoint = format!("/api/{}/", T::endpoint());
204        debug!(endpoint, "Loading");
205
206        let items: Vec<T> = self.fetch_all_pages(&endpoint, None).await?;
207        Ok(items.into_iter().map(|item| (item.id(), item)).collect())
208    }
209
210    fn default_query_params(&self) -> Option<HashMap<&'static str, Cow<'_, str>>> {
211        let mut params = HashMap::new();
212
213        if self.request_full_permissions {
214            params.insert(
215                crate::document_query::QUERY_PARAM_FULL_PERMISSIONS,
216                Cow::Borrowed("true"),
217            );
218        }
219        if !self.request_full_content {
220            params.insert(
221                crate::document_query::QUERY_PARAM_TRUNCATE_CONTENT,
222                Cow::Borrowed("true"),
223            );
224        }
225
226        if params.is_empty() {
227            None
228        } else {
229            Some(params)
230        }
231    }
232
233    /// Refresh and cache all metadata.
234    ///
235    /// Only updates the cache for this instance, cloned instances will not see the changes.
236    pub async fn refresh_all(&mut self) -> Result<()> {
237        self.refresh(enum_iterator::all::<RefreshMetaData>()).await
238    }
239
240    /// Refresh and cache the selected metadata.
241    ///
242    /// Only updates the cache for this instance, cloned instances will not see the changes.
243    ///
244    /// # Arguments
245    ///
246    /// * `data` - The metadata to refresh.
247    /// * `full_permissions` - Whether to use request full permissions data for the items being refreshed.
248    pub async fn refresh(&mut self, data: impl IntoIterator<Item = RefreshMetaData>) -> Result<()> {
249        #[rustfmt::skip]
250        async fn inner(
251            client: &mut PaperlessClient,
252            data: &mut dyn Iterator<Item = RefreshMetaData>,
253        ) -> Result<()> {
254            let selected: std::collections::HashSet<_> = data.into_iter().collect();
255
256            if selected.is_empty() {
257                return Ok(());
258            }
259
260            let (tags, custom_fields, correspondents, document_types, groups, users, storage_paths) =
261                futures_util::try_join!(
262                    async {
263                        if selected.contains(&RefreshMetaData::Tags) {
264                            Ok(Some(client.load_items::<Tag>().await?))
265                        } else {
266                            Ok::<Option<_>, Error>(None)
267                        }
268                    },
269                    async {
270                        if selected.contains(&RefreshMetaData::CustomFields) {
271                            Ok(Some(client.load_items::<CustomField>().await?))
272                        } else {
273                            Ok(None)
274                        }
275                    },
276                    async {
277                        if selected.contains(&RefreshMetaData::Correspondents) {
278                            Ok(Some(client.load_items::<Correspondent>().await?))
279                        } else {
280                            Ok(None)
281                        }
282                    },
283                    async {
284                        if selected.contains(&RefreshMetaData::DocumentTypes) {
285                            Ok(Some(client.load_items::<DocumentType>().await?))
286                        } else {
287                            Ok(None)
288                        }
289                    },
290                    async {
291                        if selected.contains(&RefreshMetaData::Groups) {
292                            Ok(Some(client.load_items::<Group>().await?))
293                        } else {
294                            Ok(None)
295                        }
296                    },
297                    async {
298                        if selected.contains(&RefreshMetaData::Users) {
299                            Ok(Some(client.load_items::<User>().await?))
300                        } else {
301                            Ok(None)
302                        }
303                    },
304                    async {
305                        if selected.contains(&RefreshMetaData::StoragePaths) {
306                            Ok(Some(client.load_items::<StoragePath>().await?))
307                        } else {
308                            Ok(None)
309                        }
310                    },
311                )?;
312
313            let cached_data = Arc::make_mut(&mut client.cached_data);
314
315            if let Some(value) = custom_fields { cached_data.custom_fields = value; }
316            if let Some(value) = correspondents { cached_data.correspondents = value; }
317            if let Some(value) = document_types { cached_data.document_types = value; }
318            if let Some(value) = groups { cached_data.groups = value; }
319            if let Some(value) = storage_paths { cached_data.storage_paths = value; }
320            if let Some(value) = tags { cached_data.tags = value; }
321            if let Some(value) = users { cached_data.users = value; }
322
323            Ok(())
324        }
325
326        inner(self, &mut data.into_iter()).await
327    }
328
329    /// Query documents using the given [`DocumentQueryBuilder`].
330    pub async fn query_documents(&self, query: DocumentQueryBuilder) -> Result<Vec<Document>> {
331        let full_content = query.full_content;
332        let query_params = query.build();
333        let query: HashMap<&str, Cow<str>> = query_params
334            .query
335            .into_iter()
336            .map(|(k, v)| (k, Cow::Owned(v)))
337            .collect();
338
339        let documents: Vec<_> = self
340            .fetch_all_pages::<DocumentData>("/api/documents/", Some(&query))
341            .await?
342            .into_iter()
343            .map(|data| Document::new(data, Arc::new(self.clone()), !full_content))
344            .collect();
345
346        Ok(documents)
347    }
348
349    /// Get all documents with any of the given tags.
350    pub fn get_documents_by_tags(
351        &self,
352        tag_ids: &[TagId],
353    ) -> impl Future<Output = Result<Vec<Document>>> {
354        let query = DocumentQueryBuilder::default()
355            .full_content(self.request_full_content)
356            .full_permissions(self.request_full_permissions)
357            .tags_id_in(tag_ids.to_vec());
358
359        self.query_documents(query)
360    }
361
362    pub(crate) async fn get_document_data_by_id(
363        &self,
364        id: DocumentId,
365        full_content: Option<bool>,
366        full_permissions: Option<bool>,
367    ) -> Result<DocumentData> {
368        let mut params = self.default_query_params();
369
370        if full_content.is_some() || full_permissions.is_some() {
371            let mut updated_params = params.unwrap_or_default();
372
373            if let Some(full_content) = full_content {
374                updated_params.insert(
375                    crate::document_query::QUERY_PARAM_TRUNCATE_CONTENT,
376                    Cow::Owned((!full_content).to_string()),
377                );
378            }
379
380            if let Some(full_permissions) = full_permissions {
381                updated_params.insert(
382                    crate::document_query::QUERY_PARAM_FULL_PERMISSIONS,
383                    Cow::Owned(full_permissions.to_string()),
384                );
385            }
386
387            params = Some(updated_params);
388        }
389
390        self.request_json_no_body(
391            Method::GET,
392            &format!("/api/documents/{}/", id.0),
393            params.as_ref(),
394        )
395        .await
396    }
397
398    /// Get a document by its ID.
399    pub async fn get_document_by_id(
400        &self,
401        id: DocumentId,
402        full_content: Option<bool>,
403        full_permissions: Option<bool>,
404    ) -> Result<Document> {
405        let content_is_truncated = !full_content.unwrap_or(self.request_full_content);
406        Ok(Document::new(
407            self.get_document_data_by_id(id, full_content, full_permissions)
408                .await?,
409            Arc::new(self.clone()),
410            content_is_truncated,
411        ))
412    }
413
414    /// Make a request and parse the response as JSON.
415    pub(crate) fn request_json_no_body<T: serde::de::DeserializeOwned>(
416        &self,
417        method: Method,
418        endpoint: &str,
419        query_params: Option<&HashMap<&str, Cow<str>>>,
420    ) -> impl Future<Output = Result<T>> {
421        self.request_json(method, endpoint, None::<&()>, query_params)
422    }
423
424    /// Make a request and parse the response as JSON.
425    pub(crate) async fn request_json<T: serde::de::DeserializeOwned>(
426        &self,
427        method: Method,
428        endpoint: &str,
429        body: Option<&impl Serialize>,
430        query_params: Option<&HashMap<&str, Cow<'_, str>>>,
431    ) -> Result<T> {
432        let resp = self.request(method, endpoint, body, query_params).await?;
433
434        if tracing::enabled!(tracing::Level::TRACE) {
435            // Only log the response body if trace logging is enabled to avoid unnecessary overhead
436            let response_text = resp.text().await.unwrap_or_default();
437            trace!(body = %response_text, "Response");
438
439            Ok(serde_json::from_str(&response_text)
440                .map_err(|e| Error::InvalidJson(format!("Failed to parse response body: {e:?}")))?)
441        } else {
442            Ok(resp
443                .json()
444                .await
445                .map_err(|e| Error::InvalidJson(format!("Failed to parse response body: {e:?}")))?)
446        }
447    }
448
449    /// Make a request and return the raw [`reqwest::Response`].
450    pub(crate) fn request_no_body(
451        &self,
452        method: Method,
453        endpoint: &str,
454        query_params: Option<&HashMap<&str, Cow<'_, str>>>,
455    ) -> impl Future<Output = Result<reqwest::Response>> {
456        self.request(method, endpoint, None::<&()>, query_params)
457    }
458
459    /// Make a request and return the raw [`reqwest::Response`].
460    pub(crate) async fn request(
461        &self,
462        method: Method,
463        endpoint: &str,
464        body: Option<&impl Serialize>,
465        query_params: Option<&HashMap<&str, Cow<'_, str>>>,
466    ) -> Result<reqwest::Response> {
467        let mut req = self
468            .client
469            .request(method, format!("{}{endpoint}", self.base_url))
470            .header(ACCEPT, "application/json");
471
472        if let Some(params) = query_params {
473            req = req.query(params);
474        }
475
476        // Set payload body if provided
477        if let Some(json_body) = body {
478            req = req.json(json_body);
479        }
480
481        let req = req.build().map_err(|e| Error::Request(e.into()))?;
482
483        if tracing::enabled!(tracing::Level::TRACE)
484            && let Some(body) = req.body().and_then(|b| b.as_bytes())
485        {
486            trace!(
487                method = ?req.method(),
488                url = ?req.url(),
489                body = %String::from_utf8_lossy(body),
490                "Sending request to Paperless API");
491        } else {
492            debug!(
493                method = ?req.method(),
494                url = ?req.url(),
495                "Sending request to Paperless API");
496        }
497
498        let resp = self
499            .client
500            .execute(req)
501            .await
502            .map_err(|e| Error::Other(format!("Failed to send request: {e}")))?;
503
504        // Log the response body for debugging
505        debug!(status = ?resp.status(), "Response");
506
507        if resp.status() == StatusCode::NOT_FOUND {
508            return Err(Error::NotFound);
509        }
510
511        if !resp.status().is_success() {
512            return Err(Error::Response {
513                status_code: resp.status().as_u16(),
514                body: resp.text().await.unwrap_or_default(),
515            });
516        }
517
518        Ok(resp)
519    }
520
521    pub(crate) async fn fetch_all_pages<T: for<'de> Deserialize<'de>>(
522        &self,
523        endpoint: &str,
524        query_params: Option<&HashMap<&str, Cow<'_, str>>>,
525    ) -> Result<Vec<T>> {
526        let mut results = vec![];
527        let mut all_query_params = self.default_query_params().unwrap_or_default();
528        if let Some(query_params) = query_params {
529            all_query_params.extend(query_params.clone());
530        }
531
532        let mut all_query_params = Some(all_query_params);
533
534        let mut current_url = Some(endpoint.to_string());
535
536        while let Some(url) = current_url {
537            debug!("Fetching page: {url}");
538
539            let page: PaginatedResponse<T> = self
540                .request_json_no_body(Method::GET, &url, all_query_params.as_ref())
541                .await?;
542
543            results.extend(page.results);
544
545            current_url = page.next.and_then(|next_url| {
546                // Extract just the path from the full URL
547                next_url
548                    .strip_prefix(&*self.base_url)
549                    .unwrap_or(&next_url)
550                    .to_string()
551                    .into()
552            });
553            all_query_params = None;
554        }
555
556        Ok(results)
557    }
558
559    /// Get all tasks with optional filtering by ID, name, or acknowledged status.
560    pub async fn get_task_status(
561        &self,
562        task_id: Option<&TaskId>,
563        task_name: Option<&str>,
564        acknowledged: Option<bool>,
565    ) -> Result<Vec<Task>> {
566        let mut query = Vec::new();
567
568        if let Some(id) = task_id {
569            query.push(("task_id", id.to_string()));
570        }
571
572        if let Some(name) = task_name {
573            query.push(("task_name", name.to_string()));
574        }
575
576        if let Some(ack) = acknowledged {
577            query.push(("acknowledged", ack.to_string()));
578        }
579
580        let resp = self
581            .request_no_body(
582                Method::GET,
583                &format!(
584                    "/api/tasks/?{}",
585                    serde_urlencoded::to_string(&query)
586                        .map_err(|e| Error::Other(format!("Failed to serialize query: {e}")))?
587                ),
588                None,
589            )
590            .await?;
591
592        let body = resp
593            .text()
594            .await
595            .map_err(|e| Error::Other(format!("Failed to read response body: {e:?}")))?;
596
597        trace!("get_task_status response: {:?}", body);
598
599        let tasks: Vec<Task> = match serde_json::from_str(&body) {
600            Ok(t) => t,
601            Err(e) => {
602                return Err(Error::InvalidJson(format!(
603                    "Failed to parse response body: {e:?}"
604                )));
605            }
606        };
607
608        if tasks.is_empty() {
609            return Err(Error::NotFound);
610        }
611
612        Ok(tasks)
613    }
614
615    /// Get all workflows.
616    pub fn get_workflows(&self) -> impl Future<Output = Result<Vec<Workflow>>> {
617        self.fetch_all_pages("/api/workflows/", None)
618    }
619
620    /// Get all saved views.
621    pub fn get_saved_views(&self) -> impl Future<Output = Result<Vec<SavedView>>> {
622        self.fetch_all_pages("/api/saved_views/", None)
623    }
624
625    /// Get server statistics.
626    pub fn get_statistics(&self) -> impl Future<Output = Result<util::Statistics>> {
627        self.request_json_no_body(Method::GET, "/api/statistics/", None)
628    }
629
630    /// Get server status.
631    pub fn get_status(&self) -> impl Future<Output = Result<util::ServerStatus>> {
632        self.request_json_no_body(Method::GET, "/api/status/", None)
633    }
634
635    /// Create a new item on the server.
636    ///
637    /// All structs which implement [`CreateDto`] can be used as `new_item`.
638    ///
639    /// Returns the created item.
640    pub async fn create<T>(&self, new_item: &T) -> Result<T::BaseType>
641    where
642        T: CreateDto,
643        T::BaseType: Item,
644    {
645        let url = format!("/api/{}/", <T::BaseType as Item>::endpoint());
646        self.request_json(Method::POST, &url, Some(&new_item), None)
647            .await
648    }
649
650    /// Updates an existing item.
651    ///
652    /// All structs which implement [`UpdateDto`] can be used as `item`.
653    ///
654    /// Returns the updated item
655    pub async fn update<T>(&self, id: T::Id, update: &T) -> Result<T::BaseType>
656    where
657        T: UpdateDto,
658        T::BaseType: Item,
659    {
660        let url = format!("/api/{}/{}/", <T::BaseType as Item>::endpoint(), id);
661        self.request_json::<T::BaseType>(Method::PATCH, &url, Some(&update), None)
662            .await
663    }
664
665    /// Deletes an existing item.
666    ///
667    /// Can be used for all [`ItemId`]s
668    pub async fn delete<T: ItemId>(&self, id: T) -> Result<()> {
669        let url = format!("/api/{}/{}/", T::endpoint(), id);
670        self.request_no_body(Method::DELETE, &url, None).await?;
671        Ok(())
672    }
673
674    /// Load an existing item directly from the server, bypassing the caches.
675    ///
676    /// All structs which implement [`Item`] can be used.
677    pub async fn load_by_id<T: Item>(&self, id: T::Id) -> Result<Option<T>> {
678        let url = format!("/api/{}/{}/", T::endpoint(), id);
679        match self.request_json_no_body(Method::GET, &url, None).await {
680            found_item @ Ok(_) => found_item,
681            Err(Error::NotFound) => Ok(None),
682            err @ Err(_) => err,
683        }
684    }
685
686    /// Upload a document to Paperless.
687    ///
688    /// Returns the task ID on success.
689    pub async fn upload_document(&self, file_path: &Path, filename: &str) -> Result<TaskId> {
690        let stream = tokio::fs::File::open(file_path)
691            .await
692            .map_err(|e| Error::Other(format!("Failed to open file: {e}")))?;
693
694        let form = multipart::Form::new().part(
695            "document",
696            multipart::Part::stream(stream).file_name(filename.to_string()),
697        );
698
699        let url = format!("{}/api/documents/post_document/", self.base_url);
700
701        let resp = self
702            .client
703            .post(&url)
704            .multipart(form)
705            .send()
706            .await
707            .map_err(|e| Error::Other(format!("Failed to send request: {e}")))?;
708
709        let status = resp.status();
710        if !resp.status().is_success() {
711            return Err(Error::Response {
712                status_code: status.as_u16(),
713                body: resp.text().await.unwrap_or_default(),
714            });
715        }
716
717        let task_id: String = resp
718            .json()
719            .await
720            .map_err(|e| Error::Other(format!("Failed to parse task ID: {e:?}")))?;
721        Ok(TaskId(task_id))
722    }
723
724    /// Get the tags cache.
725    #[inline]
726    #[must_use]
727    pub fn tags(&self) -> &HashMap<TagId, Tag> {
728        &self.cached_data.tags
729    }
730
731    /// Get the storage paths cache.
732    #[inline]
733    #[must_use]
734    pub fn storage_paths(&self) -> &HashMap<StoragePathId, StoragePath> {
735        &self.cached_data.storage_paths
736    }
737
738    /// Find a tag by its name.
739    #[must_use]
740    pub fn find_tag_by_name(&self, name: &str) -> Option<&Tag> {
741        self.cached_data.tags.values().find(|tag| tag.name == name)
742    }
743
744    /// Get the document types cache.
745    #[inline]
746    #[must_use]
747    pub fn document_types(&self) -> &HashMap<DocumentTypeId, DocumentType> {
748        &self.cached_data.document_types
749    }
750
751    /// Find a document type by its name.
752    #[must_use]
753    pub fn find_document_type_by_name(&self, name: &str) -> Option<&DocumentType> {
754        self.cached_data
755            .document_types
756            .values()
757            .find(|dt| dt.name == name)
758    }
759
760    /// Search for documents.
761    pub async fn search(&self, search: &str) -> Result<Vec<(Document, SearchHit)>> {
762        let results = self
763            .fetch_all_pages::<SearchResult>(
764                "/api/documents/",
765                Some(&HashMap::from([("query", search.into())])),
766            )
767            .await?
768            .into_iter()
769            .map(|result| {
770                (
771                    Document::new(
772                        result.document_data,
773                        Arc::new(self.clone()),
774                        !self.request_full_content,
775                    ),
776                    result.search_hit,
777                )
778            })
779            .collect();
780        Ok(results)
781    }
782
783    /// Get the correspondents cache.
784    #[inline]
785    #[must_use]
786    pub fn correspondents(&self) -> &HashMap<CorrespondentId, Correspondent> {
787        &self.cached_data.correspondents
788    }
789
790    /// Get the custom fields cache.
791    #[inline]
792    #[must_use]
793    pub fn custom_fields(&self) -> &HashMap<CustomFieldId, CustomField> {
794        &self.cached_data.custom_fields
795    }
796
797    /// Find a custom field by its name.
798    #[must_use]
799    pub fn find_custom_field_by_name(&self, name: &str) -> Option<&CustomField> {
800        self.cached_data
801            .custom_fields
802            .values()
803            .find(|field| field.name == name)
804    }
805
806    /// Get the users cache.
807    #[inline]
808    #[must_use]
809    pub fn users(&self) -> &HashMap<UserId, User> {
810        &self.cached_data.users
811    }
812
813    /// Get the groups cache.
814    #[inline]
815    #[must_use]
816    pub fn groups(&self) -> &HashMap<GroupId, Group> {
817        &self.cached_data.groups
818    }
819}