Skip to main content

ekodb_client/
client.rs

1//! Main client implementation for ekoDB
2
3use crate::auth::AuthManager;
4use crate::error::{Error, Result};
5use crate::http::HttpClient;
6use crate::schema::{CollectionMetadata, Schema};
7use crate::search::{DistinctValuesQuery, DistinctValuesResponse, SearchQuery, SearchResponse};
8use crate::types::{FieldType, Query, Record};
9use std::sync::Arc;
10use std::time::Duration;
11
12/// Rate limit information from the server
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct RateLimitInfo {
15    /// Maximum requests allowed per window
16    pub limit: usize,
17    /// Requests remaining in current window
18    pub remaining: usize,
19    /// Unix timestamp when the rate limit resets
20    pub reset: i64,
21}
22
23impl RateLimitInfo {
24    /// Check if the rate limit is close to being exceeded
25    ///
26    /// Returns true if remaining requests are less than 10% of the limit
27    pub fn is_near_limit(&self) -> bool {
28        let threshold = (self.limit as f64 * 0.1) as usize;
29        self.remaining <= threshold
30    }
31
32    /// Check if the rate limit has been exceeded
33    pub fn is_exceeded(&self) -> bool {
34        self.remaining == 0
35    }
36
37    /// Get the percentage of requests remaining
38    pub fn remaining_percentage(&self) -> f64 {
39        (self.remaining as f64 / self.limit as f64) * 100.0
40    }
41}
42
43/// ekoDB client
44#[derive(Clone)]
45pub struct Client {
46    http: Arc<HttpClient>,
47    auth: Arc<AuthManager>,
48    /// Opt-in schema cache for primary_key_alias resolution and field validation.
49    schema_cache: Arc<crate::schema_cache::SchemaCache>,
50    /// Base URL stored for WS URL derivation in connect_ws()
51    base_url: String,
52}
53
54impl Client {
55    /// Create a new client builder
56    pub fn builder() -> ClientBuilder {
57        ClientBuilder::default()
58    }
59
60    /// Health check
61    pub async fn health_check(&self) -> Result<()> {
62        self.http.health_check().await
63    }
64
65    /// Execute an operation with automatic token refresh on TokenExpired errors
66    async fn execute_with_token_refresh<F, Fut, T>(&self, mut operation: F) -> Result<T>
67    where
68        F: FnMut(String) -> Fut,
69        Fut: std::future::Future<Output = Result<T>>,
70    {
71        // First attempt with current token
72        let token = self.auth.get_token().await?;
73        match operation(token).await {
74            Ok(result) => Ok(result),
75            Err(Error::TokenExpired) => {
76                // Token expired, refresh and retry once
77                log::debug!("Token expired, refreshing and retrying...");
78                let new_token = self.auth.refresh_token().await?;
79                operation(new_token).await
80            }
81            Err(e) => Err(e),
82        }
83    }
84
85    /// Insert a record into a collection
86    ///
87    /// # Arguments
88    ///
89    /// * `collection` - The collection name
90    /// * `record` - The record to insert
91    /// * `options` - Optional insert options (TTL, bypass_ripple, transaction_id, bypass_cache)
92    ///
93    /// # Returns
94    ///
95    /// The inserted record with server-generated fields (e.g., `id`, `_created_at`)
96    ///
97    /// # Example
98    ///
99    /// ```no_run
100    /// # use ekodb_client::{Client, Record};
101    /// # use ekodb_client::options::InsertOptions;
102    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
103    /// let client = Client::builder()
104    ///     .base_url("https://your-instance.ekodb.net")
105    ///     .api_key("your-token")
106    ///     .build()?;
107    ///
108    /// let mut record = Record::new();
109    /// record.insert("name", "John Doe");
110    /// record.insert("age", 30);
111    ///
112    /// // Simple insert
113    /// let result = client.insert("users", record.clone(), None).await?;
114    ///
115    /// // Insert with TTL (expires in 1 hour)
116    /// let options = InsertOptions::new().ttl("1h");
117    /// let result = client.insert("sessions", record, Some(options)).await?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub async fn insert(
122        &self,
123        collection: &str,
124        record: Record,
125        options: Option<crate::options::InsertOptions>,
126    ) -> Result<Record> {
127        let collection = collection.to_string();
128        let http = self.http.clone();
129        self.execute_with_token_refresh(move |token| {
130            let collection = collection.clone();
131            let record = record.clone();
132            let http = http.clone();
133            let options = options.clone();
134            async move { http.insert(&collection, record, options, &token).await }
135        })
136        .await
137    }
138
139    /// Find records in a collection
140    ///
141    /// # Arguments
142    ///
143    /// * `collection` - The collection name
144    /// * `query` - The query to filter records
145    ///
146    /// # Returns
147    ///
148    /// A vector of matching records
149    ///
150    /// # Example
151    ///
152    /// ```no_run
153    /// # use ekodb_client::{Client, Query};
154    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
155    /// let client = Client::builder()
156    ///     .base_url("https://your-instance.ekodb.net")
157    ///     .api_key("your-api-key")
158    ///     .build()?;
159    ///
160    /// let query = Query::new()
161    ///     .filter(serde_json::json!({
162    ///         "type": "Condition",
163    ///         "content": {
164    ///             "field": "age",
165    ///             "operator": "Gte",
166    ///             "value": 18
167    ///         }
168    ///     }))
169    ///     .limit(10);
170    /// let results = client.find("users", query, None).await?;
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub async fn find(
175        &self,
176        collection: &str,
177        query: Query,
178        bypass_ripple: Option<bool>,
179    ) -> Result<Vec<Record>> {
180        let collection = collection.to_string();
181        let http = self.http.clone();
182        self.execute_with_token_refresh(move |token| {
183            let collection = collection.clone();
184            let query = query.clone();
185            let http = http.clone();
186            async move { http.find(&collection, query, &token, bypass_ripple).await }
187        })
188        .await
189    }
190
191    /// Find a single record by ID
192    ///
193    /// # Arguments
194    ///
195    /// * `collection` - The collection name
196    /// * `id` - The record ID
197    ///
198    /// # Returns
199    ///
200    /// The record if found, or `Error::NotFound` if not found
201    pub async fn find_by_id(
202        &self,
203        collection: &str,
204        id: &str,
205        bypass_ripple: Option<bool>,
206    ) -> Result<Record> {
207        let collection = collection.to_string();
208        let id = id.to_string();
209        let http = self.http.clone();
210        self.execute_with_token_refresh(move |token| {
211            let collection = collection.clone();
212            let id = id.clone();
213            let http = http.clone();
214            async move {
215                http.find_by_id(&collection, &id, &token, bypass_ripple)
216                    .await
217            }
218        })
219        .await
220    }
221
222    /// Find a record by ID, returning only a projection of its fields.
223    ///
224    /// `select_fields` keeps only the named fields (plus the primary key);
225    /// `exclude_fields` removes the named fields. Pass `None`/empty for either.
226    pub async fn find_by_id_with_projection(
227        &self,
228        collection: &str,
229        id: &str,
230        select_fields: Option<Vec<String>>,
231        exclude_fields: Option<Vec<String>>,
232    ) -> Result<Record> {
233        let collection = collection.to_string();
234        let id = id.to_string();
235        let http = self.http.clone();
236        self.execute_with_token_refresh(move |token| {
237            let collection = collection.clone();
238            let id = id.clone();
239            let select_fields = select_fields.clone();
240            let exclude_fields = exclude_fields.clone();
241            let http = http.clone();
242            async move {
243                http.find_by_id_with_projection(
244                    &collection,
245                    &id,
246                    select_fields.as_deref(),
247                    exclude_fields.as_deref(),
248                    &token,
249                )
250                .await
251            }
252        })
253        .await
254    }
255
256    /// Update a record by ID
257    ///
258    /// # Arguments
259    ///
260    /// * `collection` - The collection name
261    /// * `id` - The record ID
262    /// * `record` - The updated record data
263    /// * `options` - Optional update options (bypass_ripple, transaction_id, bypass_cache)
264    ///
265    /// # Returns
266    ///
267    /// The updated record
268    pub async fn update(
269        &self,
270        collection: &str,
271        id: &str,
272        record: Record,
273        options: Option<crate::options::UpdateOptions>,
274    ) -> Result<Record> {
275        let collection = collection.to_string();
276        let id = id.to_string();
277        let http = self.http.clone();
278        self.execute_with_token_refresh(move |token| {
279            let collection = collection.clone();
280            let id = id.clone();
281            let record = record.clone();
282            let options = options.clone();
283            let http = http.clone();
284            async move { http.update(&collection, &id, record, options, &token).await }
285        })
286        .await
287    }
288
289    /// Apply an atomic field action to a single field of a record.
290    ///
291    /// Use this instead of `update()` when you need safe concurrent modifications
292    /// like incrementing counters, pushing to arrays, or arithmetic operations.
293    ///
294    /// # Actions
295    ///
296    /// - `increment` / `decrement` — add or subtract a number
297    /// - `multiply` / `divide` / `modulo` — arithmetic on numeric fields
298    /// - `push` / `unshift` — append or prepend a value to an array
299    /// - `pop` / `shift` — remove last or first element of an array
300    /// - `remove` — remove a specific value from an array
301    /// - `append` — concatenate a string onto a string field
302    /// - `clear` — reset a field to its zero value (0, "", [])
303    ///
304    /// # Arguments
305    ///
306    /// * `collection` - The collection name
307    /// * `id` - The record ID
308    /// * `action` - The atomic action to apply
309    /// * `field` - The field name to apply the action to
310    /// * `value` - The value for the action (use `FieldType::Null` for pop/shift/clear)
311    pub async fn update_with_action(
312        &self,
313        collection: &str,
314        id: &str,
315        action: &str,
316        field: &str,
317        value: FieldType,
318    ) -> Result<Record> {
319        let collection = collection.to_string();
320        let id = id.to_string();
321        let action = action.to_string();
322        let field = field.to_string();
323        let http = self.http.clone();
324        self.execute_with_token_refresh(move |token| {
325            let collection = collection.clone();
326            let id = id.clone();
327            let action = action.clone();
328            let field = field.clone();
329            let value = value.clone();
330            let http = http.clone();
331            async move {
332                http.update_with_action(&collection, &id, &action, &field, value, &token)
333                    .await
334            }
335        })
336        .await
337    }
338
339    /// Apply a sequence of atomic field actions to a record in a single request.
340    ///
341    /// All actions are applied atomically — the record is fetched once, all actions
342    /// run in order, and the result is persisted in a single update.
343    ///
344    /// # Arguments
345    ///
346    /// * `collection` - The collection name
347    /// * `id` - The record ID
348    /// * `actions` - A list of (action, field, value) tuples
349    pub async fn update_with_action_sequence(
350        &self,
351        collection: &str,
352        id: &str,
353        actions: Vec<(String, String, FieldType)>,
354    ) -> Result<Record> {
355        let collection = collection.to_string();
356        let id = id.to_string();
357        let http = self.http.clone();
358        self.execute_with_token_refresh(move |token| {
359            let collection = collection.clone();
360            let id = id.clone();
361            let actions = actions.clone();
362            let http = http.clone();
363            async move {
364                http.update_with_action_sequence(&collection, &id, actions, &token)
365                    .await
366            }
367        })
368        .await
369    }
370
371    /// Delete a record by ID
372    ///
373    /// # Arguments
374    ///
375    /// * `collection` - The collection name
376    /// * `id` - The record ID
377    ///
378    /// # Returns
379    ///
380    /// `Ok(())` if the record was deleted successfully
381    pub async fn delete(
382        &self,
383        collection: &str,
384        id: &str,
385        bypass_ripple: Option<bool>,
386    ) -> Result<()> {
387        let collection = collection.to_string();
388        let id = id.to_string();
389        let http = self.http.clone();
390        self.execute_with_token_refresh(move |token| {
391            let collection = collection.clone();
392            let id = id.clone();
393            let http = http.clone();
394            async move { http.delete(&collection, &id, &token, bypass_ripple).await }
395        })
396        .await
397    }
398
399    /// Delete a record with options — notably `transaction_id` for a staged,
400    /// buffered transactional delete (applied at commit).
401    pub async fn delete_with_options(
402        &self,
403        collection: &str,
404        id: &str,
405        options: crate::options::DeleteOptions,
406    ) -> Result<()> {
407        let collection = collection.to_string();
408        let id = id.to_string();
409        let http = self.http.clone();
410        self.execute_with_token_refresh(move |token| {
411            let collection = collection.clone();
412            let id = id.clone();
413            let options = options.clone();
414            let http = http.clone();
415            async move {
416                http.delete_with_options(&collection, &id, &token, &options)
417                    .await
418            }
419        })
420        .await
421    }
422
423    /// Restore a deleted record from trash (undelete)
424    ///
425    /// # Arguments
426    ///
427    /// * `collection` - The collection name
428    /// * `id` - The record ID to restore
429    ///
430    /// # Returns
431    ///
432    /// `Ok(true)` if the record was restored, `Ok(false)` if no tombstone was found
433    ///
434    /// # Example
435    ///
436    /// ```no_run
437    /// # use ekodb_client::Client;
438    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
439    /// let client = Client::builder()
440    ///     .base_url("https://your-instance.ekodb.net")
441    ///     .api_key("your-token")
442    ///     .build()?;
443    ///
444    /// // Delete a record
445    /// client.delete("users", "user123", None).await?;
446    ///
447    /// // Restore it from trash
448    /// let restored = client.restore_deleted("users", "user123").await?;
449    /// if restored {
450    ///     println!("Record restored successfully");
451    /// }
452    /// # Ok(())
453    /// # }
454    /// ```
455    pub async fn restore_deleted(&self, collection: &str, id: &str) -> Result<bool> {
456        let collection = collection.to_string();
457        let id = id.to_string();
458        let http = self.http.clone();
459        self.execute_with_token_refresh(move |token| {
460            let collection = collection.clone();
461            let id = id.clone();
462            let http = http.clone();
463            async move { http.restore_deleted(&collection, &id, &token).await }
464        })
465        .await
466    }
467
468    /// Restore all deleted records in a collection from trash
469    ///
470    /// # Arguments
471    ///
472    /// * `collection` - The collection name
473    ///
474    /// # Returns
475    ///
476    /// Number of records restored
477    ///
478    /// # Example
479    ///
480    /// ```no_run
481    /// # use ekodb_client::Client;
482    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
483    /// let count = client.restore_collection("users").await?;
484    /// println!("Restored {} records", count);
485    /// # Ok(())
486    /// # }
487    /// ```
488    pub async fn restore_collection(&self, collection: &str) -> Result<usize> {
489        let collection = collection.to_string();
490        let http = self.http.clone();
491        self.execute_with_token_refresh(move |token| {
492            let collection = collection.clone();
493            let http = http.clone();
494            async move { http.restore_collection(&collection, &token).await }
495        })
496        .await
497    }
498
499    /// Batch insert multiple documents
500    ///
501    /// # Arguments
502    ///
503    /// * `collection` - The collection name
504    /// * `records` - Vector of records to insert
505    ///
506    /// # Returns
507    ///
508    /// Vector of inserted records with their IDs
509    pub async fn batch_insert(
510        &self,
511        collection: &str,
512        records: Vec<Record>,
513        bypass_ripple: Option<bool>,
514    ) -> Result<Vec<Record>> {
515        let collection = collection.to_string();
516        let http = self.http.clone();
517        self.execute_with_token_refresh(move |token| {
518            let collection = collection.clone();
519            let records = records.clone();
520            let http = http.clone();
521            async move {
522                http.batch_insert(&collection, records, &token, bypass_ripple, None)
523                    .await
524            }
525        })
526        .await
527    }
528
529    /// Batch insert with options (`bypass_ripple` and/or a `transaction_id` to
530    /// stage the inserts into an MVCC transaction instead of committing them
531    /// immediately).
532    ///
533    /// # Arguments
534    ///
535    /// * `collection` - The collection name
536    /// * `records` - Vector of records to insert
537    /// * `options` - Batch insert options (`bypass_ripple`, `transaction_id`)
538    pub async fn batch_insert_with_options(
539        &self,
540        collection: &str,
541        records: Vec<Record>,
542        options: crate::options::BatchInsertOptions,
543    ) -> Result<Vec<Record>> {
544        let collection = collection.to_string();
545        let http = self.http.clone();
546        self.execute_with_token_refresh(move |token| {
547            let collection = collection.clone();
548            let records = records.clone();
549            let http = http.clone();
550            let options = options.clone();
551            async move {
552                http.batch_insert(
553                    &collection,
554                    records,
555                    &token,
556                    options.bypass_ripple,
557                    options.transaction_id.as_deref(),
558                )
559                .await
560            }
561        })
562        .await
563    }
564
565    /// Batch update multiple documents
566    ///
567    /// # Arguments
568    ///
569    /// * `collection` - The collection name
570    /// * `updates` - Vector of (id, record) pairs to update
571    ///
572    /// # Returns
573    ///
574    /// Vector of updated records
575    pub async fn batch_update(
576        &self,
577        collection: &str,
578        updates: Vec<(String, Record)>,
579        bypass_ripple: Option<bool>,
580    ) -> Result<Vec<Record>> {
581        let collection = collection.to_string();
582        let http = self.http.clone();
583        self.execute_with_token_refresh(move |token| {
584            let collection = collection.clone();
585            let updates = updates.clone();
586            let http = http.clone();
587            async move {
588                http.batch_update(&collection, updates, &token, bypass_ripple, None)
589                    .await
590            }
591        })
592        .await
593    }
594
595    /// Batch update with options (`bypass_ripple` and/or a `transaction_id` to
596    /// stage the updates into an MVCC transaction instead of committing them
597    /// immediately).
598    ///
599    /// # Arguments
600    ///
601    /// * `collection` - The collection name
602    /// * `updates` - Vector of (id, record) pairs to update
603    /// * `options` - Batch update options (`bypass_ripple`, `transaction_id`)
604    pub async fn batch_update_with_options(
605        &self,
606        collection: &str,
607        updates: Vec<(String, Record)>,
608        options: crate::options::BatchUpdateOptions,
609    ) -> Result<Vec<Record>> {
610        let collection = collection.to_string();
611        let http = self.http.clone();
612        self.execute_with_token_refresh(move |token| {
613            let collection = collection.clone();
614            let updates = updates.clone();
615            let http = http.clone();
616            let options = options.clone();
617            async move {
618                http.batch_update(
619                    &collection,
620                    updates,
621                    &token,
622                    options.bypass_ripple,
623                    options.transaction_id.as_deref(),
624                )
625                .await
626            }
627        })
628        .await
629    }
630
631    /// Batch delete multiple documents by IDs
632    ///
633    /// # Arguments
634    ///
635    /// * `collection` - The collection name
636    /// * `ids` - Vector of document IDs to delete
637    ///
638    /// # Returns
639    ///
640    /// The number of records deleted
641    pub async fn batch_delete(
642        &self,
643        collection: &str,
644        ids: Vec<String>,
645        bypass_ripple: Option<bool>,
646    ) -> Result<u64> {
647        let collection = collection.to_string();
648        let http = self.http.clone();
649        self.execute_with_token_refresh(move |token| {
650            let collection = collection.clone();
651            let ids = ids.clone();
652            let http = http.clone();
653            async move {
654                http.batch_delete(&collection, ids, &token, bypass_ripple, None)
655                    .await
656            }
657        })
658        .await
659    }
660
661    /// Batch delete with options (`bypass_ripple` and/or a `transaction_id` to
662    /// stage the deletes into an MVCC transaction instead of committing them
663    /// immediately).
664    ///
665    /// # Arguments
666    ///
667    /// * `collection` - The collection name
668    /// * `ids` - Vector of document IDs to delete
669    /// * `options` - Batch delete options (`bypass_ripple`, `transaction_id`)
670    pub async fn batch_delete_with_options(
671        &self,
672        collection: &str,
673        ids: Vec<String>,
674        options: crate::options::BatchDeleteOptions,
675    ) -> Result<u64> {
676        let collection = collection.to_string();
677        let http = self.http.clone();
678        self.execute_with_token_refresh(move |token| {
679            let collection = collection.clone();
680            let ids = ids.clone();
681            let http = http.clone();
682            let options = options.clone();
683            async move {
684                http.batch_delete(
685                    &collection,
686                    ids,
687                    &token,
688                    options.bypass_ripple,
689                    options.transaction_id.as_deref(),
690                )
691                .await
692            }
693        })
694        .await
695    }
696
697    // ========== Convenience Methods ==========
698
699    /// Insert or update a record (upsert operation)
700    ///
701    /// Attempts to update the record first. If the record doesn't exist (NotFound error),
702    /// it will be inserted instead. This provides atomic insert-or-update semantics.
703    ///
704    /// # Arguments
705    ///
706    /// * `collection` - The collection name
707    /// * `id` - The record ID
708    /// * `record` - The record data to insert or update
709    /// * `options` - Optional upsert options (TTL, bypass_ripple, transaction_id, bypass_cache)
710    ///
711    /// # Returns
712    ///
713    /// The inserted or updated record with server-generated fields
714    ///
715    /// # Example
716    ///
717    /// ```no_run
718    /// # use ekodb_client::{Client, Record};
719    /// # use ekodb_client::options::UpsertOptions;
720    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
721    /// let client = Client::builder()
722    ///     .base_url("https://your-instance.ekodb.net")
723    ///     .api_key("your-api-key")
724    ///     .build()?;
725    ///
726    /// let mut record = Record::new();
727    /// record.insert("name", "John Doe");
728    /// record.insert("email", "john@example.com");
729    ///
730    /// // Will update if exists, insert if not
731    /// let result = client.upsert("users", "user123", record.clone(), None).await?;
732    ///
733    /// // With TTL option
734    /// let options = UpsertOptions::new().ttl("1h");
735    /// let result = client.upsert("sessions", "sess123", record, Some(options)).await?;
736    /// # Ok(())
737    /// # }
738    /// ```
739    pub async fn upsert(
740        &self,
741        collection: &str,
742        id: &str,
743        record: Record,
744        options: Option<crate::options::UpsertOptions>,
745    ) -> Result<Record> {
746        // Convert UpsertOptions to UpdateOptions for the update call
747        let update_opts = options.as_ref().map(|o| {
748            let mut opts = crate::options::UpdateOptions::new();
749            if let Some(bypass) = o.bypass_ripple {
750                opts = opts.bypass_ripple(bypass);
751            }
752            if let Some(ref tx_id) = o.transaction_id {
753                opts = opts.transaction_id(tx_id.clone());
754            }
755            if let Some(bypass) = o.bypass_cache {
756                opts = opts.bypass_cache(bypass);
757            }
758            opts
759        });
760
761        // Try update first
762        match self
763            .update(collection, id, record.clone(), update_opts)
764            .await
765        {
766            Ok(updated) => Ok(updated),
767            Err(Error::NotFound) => {
768                // Record doesn't exist, insert it
769                // Convert UpsertOptions to InsertOptions
770                let insert_opts = options.map(|o| {
771                    let mut opts = crate::options::InsertOptions::new();
772                    if let Some(ref ttl) = o.ttl {
773                        opts = opts.ttl(ttl.clone());
774                    }
775                    if let Some(bypass) = o.bypass_ripple {
776                        opts = opts.bypass_ripple(bypass);
777                    }
778                    if let Some(ref tx_id) = o.transaction_id {
779                        opts = opts.transaction_id(tx_id.clone());
780                    }
781                    if let Some(bypass) = o.bypass_cache {
782                        opts = opts.bypass_cache(bypass);
783                    }
784                    opts
785                });
786                self.insert(collection, record, insert_opts).await
787            }
788            Err(e) => Err(e),
789        }
790    }
791
792    /// Find a single record by field value
793    ///
794    /// Convenience method for finding one record matching a specific field value.
795    /// Returns `None` if no record matches, or `Some(Record)` for the first match.
796    ///
797    /// # Arguments
798    ///
799    /// * `collection` - The collection name
800    /// * `field` - The field name to search
801    /// * `value` - The value to match
802    ///
803    /// # Returns
804    ///
805    /// `Some(Record)` if found, `None` if not found
806    ///
807    /// # Example
808    ///
809    /// ```no_run
810    /// # use ekodb_client::Client;
811    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
812    /// let client = Client::builder()
813    ///     .base_url("https://your-instance.ekodb.net")
814    ///     .api_key("your-api-key")
815    ///     .build()?;
816    ///
817    /// // Find user by email
818    /// if let Some(user) = client.find_one("users", "email", "john@example.com").await? {
819    ///     println!("Found user: {:?}", user);
820    /// }
821    /// # Ok(())
822    /// # }
823    /// ```
824    pub async fn find_one(
825        &self,
826        collection: &str,
827        field: &str,
828        value: impl Into<serde_json::Value>,
829    ) -> Result<Option<Record>> {
830        use crate::query_builder::QueryBuilder;
831
832        let query = QueryBuilder::new().eq(field, value.into()).limit(1).build();
833
834        let mut results = self.find(collection, query, None).await?;
835        Ok(results.pop())
836    }
837
838    /// Check if a record exists by ID
839    ///
840    /// This is more efficient than fetching the record when you only need to check existence.
841    ///
842    /// # Arguments
843    ///
844    /// * `collection` - The collection name
845    /// * `id` - The record ID to check
846    ///
847    /// # Returns
848    ///
849    /// `true` if the record exists, `false` if it doesn't
850    ///
851    /// # Example
852    ///
853    /// ```no_run
854    /// # use ekodb_client::Client;
855    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
856    /// let client = Client::builder()
857    ///     .base_url("https://your-instance.ekodb.net")
858    ///     .api_key("your-api-key")
859    ///     .build()?;
860    ///
861    /// if client.exists("users", "user123").await? {
862    ///     println!("User exists");
863    /// } else {
864    ///     println!("User not found");
865    /// }
866    /// # Ok(())
867    /// # }
868    /// ```
869    pub async fn exists(&self, collection: &str, id: &str) -> Result<bool> {
870        match self.find_by_id(collection, id, None).await {
871            Ok(_) => Ok(true),
872            Err(Error::NotFound) => Ok(false),
873            Err(e) => Err(e),
874        }
875    }
876
877    /// Paginate through records
878    ///
879    /// Convenience method for pagination with page numbers (1-indexed).
880    ///
881    /// # Arguments
882    ///
883    /// * `collection` - The collection name
884    /// * `page` - The page number (1-indexed, i.e., first page is 1)
885    /// * `page_size` - Number of records per page
886    ///
887    /// # Returns
888    ///
889    /// A vector of records for the requested page
890    ///
891    /// # Example
892    ///
893    /// ```no_run
894    /// # use ekodb_client::Client;
895    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
896    /// let client = Client::builder()
897    ///     .base_url("https://your-instance.ekodb.net")
898    ///     .api_key("your-api-key")
899    ///     .build()?;
900    ///
901    /// // Get page 2 with 10 records per page
902    /// let records = client.paginate("users", 2, 10).await?;
903    /// # Ok(())
904    /// # }
905    /// ```
906    pub async fn paginate(
907        &self,
908        collection: &str,
909        page: usize,
910        page_size: usize,
911    ) -> Result<Vec<Record>> {
912        use crate::types::Query;
913
914        // Page 1 = skip 0, Page 2 = skip page_size, etc.
915        let skip = if page > 0 { (page - 1) * page_size } else { 0 };
916
917        let query = Query {
918            filter: None,
919            sort: None,
920            limit: Some(page_size),
921            skip: Some(skip),
922            join: None,
923            bypass_cache: None,
924            bypass_ripple: None,
925            select_fields: None,
926            exclude_fields: None,
927        };
928
929        self.find(collection, query, None).await
930    }
931
932    /// Refresh the authentication token
933    ///
934    /// Clears the cached token and fetches a new one from the server.
935    /// This is useful when you receive a 401 Unauthorized error.
936    ///
937    /// # Example
938    ///
939    /// ```no_run
940    /// # use ekodb_client::Client;
941    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
942    /// // If you get a 401 error, refresh the token
943    /// client.refresh_token().await?;
944    /// # Ok(())
945    /// # }
946    /// ```
947    /// Get a valid authentication token (JWT).
948    ///
949    /// Returns a cached token if available, otherwise exchanges the API key
950    /// for a new JWT via `/api/auth/token`.
951    pub async fn get_token(&self) -> Result<String> {
952        self.auth.get_token().await
953    }
954
955    pub async fn refresh_token(&self) -> Result<String> {
956        self.auth.refresh_token().await
957    }
958
959    /// Clear the cached authentication token
960    ///
961    /// This will force a new token to be fetched on the next request.
962    /// Useful for testing or when you know the token has expired.
963    ///
964    /// # Example
965    ///
966    /// ```no_run
967    /// # use ekodb_client::Client;
968    /// # async fn example(client: &Client) {
969    /// // Clear the cached token
970    /// client.clear_token_cache().await;
971    /// # }
972    /// ```
973    pub async fn clear_token_cache(&self) {
974        self.auth.clear_cache().await
975    }
976
977    /// List all collections
978    ///
979    /// # Returns
980    ///
981    /// A vector of collection names
982    pub async fn list_collections(&self) -> Result<Vec<String>> {
983        let http = self.http.clone();
984        self.execute_with_token_refresh(move |token| {
985            let http = http.clone();
986            async move { http.list_collections(&token).await }
987        })
988        .await
989    }
990
991    /// List collections, excluding internal chat/system collections.
992    pub async fn list_user_collections(&self) -> Result<Vec<String>> {
993        let http = self.http.clone();
994        self.execute_with_token_refresh(move |token| {
995            let http = http.clone();
996            async move { http.list_collections_filtered(&token, true).await }
997        })
998        .await
999    }
1000
1001    /// Delete a collection
1002    ///
1003    /// # Arguments
1004    ///
1005    /// * `collection` - The collection name to delete
1006    pub async fn delete_collection(&self, collection: &str) -> Result<()> {
1007        let collection = collection.to_string();
1008        let http = self.http.clone();
1009        self.execute_with_token_refresh(move |token| {
1010            let collection = collection.clone();
1011            let http = http.clone();
1012            async move { http.delete_collection(&collection, &token).await }
1013        })
1014        .await
1015    }
1016
1017    /// Count documents in a collection
1018    ///
1019    /// # Arguments
1020    ///
1021    /// * `collection` - The collection name
1022    ///
1023    /// # Returns
1024    ///
1025    /// The number of documents in the collection
1026    pub async fn count_documents(&self, collection: &str) -> Result<usize> {
1027        // Use select_fields to return only _id (minimal data transfer).
1028        // No dedicated server count endpoint exists, so we fetch IDs only.
1029        let query = Query {
1030            select_fields: Some(vec!["_id".to_string()]),
1031            ..Query::default()
1032        };
1033        let records = self.find(collection, query, None).await?;
1034        Ok(records.len())
1035    }
1036
1037    /// Check if a collection exists
1038    ///
1039    /// # Arguments
1040    ///
1041    /// * `collection` - The collection name
1042    ///
1043    /// # Returns
1044    ///
1045    /// `true` if the collection exists, `false` otherwise
1046    pub async fn collection_exists(&self, collection: &str) -> Result<bool> {
1047        let collections = self.list_collections().await?;
1048        Ok(collections.contains(&collection.to_string()))
1049    }
1050
1051    /// Set a key-value pair
1052    ///
1053    /// # Arguments
1054    ///
1055    /// * `key` - The key
1056    /// * `value` - The value (any JSON-serializable type)
1057    /// * `ttl` - Optional TTL duration (e.g., "30s", "5m", "1h")
1058    pub async fn kv_set(
1059        &self,
1060        key: &str,
1061        value: serde_json::Value,
1062        ttl: Option<&str>,
1063    ) -> Result<()> {
1064        let key = key.to_string();
1065        let ttl = ttl.map(|s| s.to_string());
1066        let http = self.http.clone();
1067        self.execute_with_token_refresh(move |token| {
1068            let key = key.clone();
1069            let value = value.clone();
1070            let ttl = ttl.clone();
1071            let http = http.clone();
1072            async move { http.kv_set(&key, value, ttl.as_deref(), &token).await }
1073        })
1074        .await
1075    }
1076
1077    /// Get a key-value pair
1078    ///
1079    /// # Arguments
1080    ///
1081    /// * `key` - The key
1082    ///
1083    /// # Returns
1084    ///
1085    /// The value if found, or `None` if not found
1086    pub async fn kv_get(&self, key: &str) -> Result<Option<serde_json::Value>> {
1087        let key = key.to_string();
1088        let http = self.http.clone();
1089        self.execute_with_token_refresh(move |token| {
1090            let key = key.clone();
1091            let http = http.clone();
1092            async move { http.kv_get(&key, &token).await }
1093        })
1094        .await
1095    }
1096
1097    /// Delete a key-value pair
1098    ///
1099    /// # Arguments
1100    ///
1101    /// * `key` - The key to delete
1102    pub async fn kv_delete(&self, key: &str) -> Result<()> {
1103        let key = key.to_string();
1104        let http = self.http.clone();
1105        self.execute_with_token_refresh(move |token| {
1106            let key = key.clone();
1107            let http = http.clone();
1108            async move { http.kv_delete(&key, &token).await }
1109        })
1110        .await
1111    }
1112
1113    /// Clear the entire KV store (all keys in the namespace).
1114    pub async fn kv_clear(&self) -> Result<()> {
1115        let http = self.http.clone();
1116        self.execute_with_token_refresh(move |token| {
1117            let http = http.clone();
1118            async move { http.kv_clear(&token).await }
1119        })
1120        .await
1121    }
1122
1123    /// Check if a key exists in the KV store
1124    ///
1125    /// # Arguments
1126    ///
1127    /// * `key` - The key to check
1128    ///
1129    /// # Returns
1130    ///
1131    /// `true` if the key exists, `false` otherwise
1132    pub async fn kv_exists(&self, key: &str) -> Result<bool> {
1133        let key = key.to_string();
1134        let http = self.http.clone();
1135        self.execute_with_token_refresh(move |token| {
1136            let key = key.clone();
1137            let http = http.clone();
1138            async move { http.kv_exists(&key, &token).await }
1139        })
1140        .await
1141    }
1142
1143    /// Batch get multiple keys
1144    ///
1145    /// # Arguments
1146    ///
1147    /// * `keys` - Vector of keys to retrieve
1148    ///
1149    /// # Returns
1150    ///
1151    /// A vector of records corresponding to the keys
1152    pub async fn kv_batch_get(&self, keys: Vec<String>) -> Result<Vec<Record>> {
1153        let http = self.http.clone();
1154        self.execute_with_token_refresh(move |token| {
1155            let keys = keys.clone();
1156            let http = http.clone();
1157            async move { http.kv_batch_get(keys, &token).await }
1158        })
1159        .await
1160    }
1161
1162    /// Batch set multiple key-value pairs
1163    ///
1164    /// # Arguments
1165    ///
1166    /// * `keys` - Vector of keys
1167    /// * `values` - Vector of values (must match length of keys)
1168    /// * `ttl` - Optional TTL in seconds (applied to all entries)
1169    ///
1170    /// # Returns
1171    ///
1172    /// Vector of tuples (key, was_set) indicating success for each operation
1173    pub async fn kv_batch_set(
1174        &self,
1175        keys: Vec<String>,
1176        values: Vec<Record>,
1177        ttl: Option<i64>,
1178    ) -> Result<Vec<(String, bool)>> {
1179        let http = self.http.clone();
1180        self.execute_with_token_refresh(move |token| {
1181            let keys = keys.clone();
1182            let values = values.clone();
1183            let http = http.clone();
1184            async move { http.kv_batch_set(keys, values, ttl, &token).await }
1185        })
1186        .await
1187    }
1188
1189    /// Batch delete multiple keys
1190    ///
1191    /// # Arguments
1192    ///
1193    /// * `keys` - Vector of keys to delete
1194    ///
1195    /// # Returns
1196    ///
1197    /// Vector of tuples (key, was_deleted) indicating success for each operation
1198    pub async fn kv_batch_delete(&self, keys: Vec<String>) -> Result<Vec<(String, bool)>> {
1199        let http = self.http.clone();
1200        self.execute_with_token_refresh(move |token| {
1201            let keys = keys.clone();
1202            let http = http.clone();
1203            async move { http.kv_batch_delete(keys, &token).await }
1204        })
1205        .await
1206    }
1207
1208    /// Query/find KV entries with pattern matching
1209    ///
1210    /// # Arguments
1211    ///
1212    /// * `pattern` - Optional regex pattern for keys (e.g., "cache:user:.*")
1213    /// * `include_expired` - Whether to include expired entries
1214    ///
1215    /// # Returns
1216    ///
1217    /// A vector of matching records
1218    pub async fn kv_find(
1219        &self,
1220        pattern: Option<&str>,
1221        include_expired: bool,
1222    ) -> Result<Vec<serde_json::Value>> {
1223        let pattern = pattern.map(|s| s.to_string());
1224        let http = self.http.clone();
1225        self.execute_with_token_refresh(move |token| {
1226            let pattern = pattern.clone();
1227            let http = http.clone();
1228            async move {
1229                http.kv_find(pattern.as_deref(), include_expired, &token)
1230                    .await
1231            }
1232        })
1233        .await
1234    }
1235
1236    /// Alias for kv_find - query KV store with pattern
1237    pub async fn kv_query(
1238        &self,
1239        pattern: Option<&str>,
1240        include_expired: bool,
1241    ) -> Result<Vec<serde_json::Value>> {
1242        self.kv_find(pattern, include_expired).await
1243    }
1244
1245    // ========== Transaction Methods ==========
1246
1247    /// Begin a new transaction
1248    ///
1249    /// # Arguments
1250    ///
1251    /// * `isolation_level` - Transaction isolation level (e.g., "ReadCommitted")
1252    ///
1253    /// # Returns
1254    ///
1255    /// The transaction ID
1256    pub async fn begin_transaction(&self, isolation_level: &str) -> Result<String> {
1257        let isolation_level = isolation_level.to_string();
1258        let http = self.http.clone();
1259        self.execute_with_token_refresh(move |token| {
1260            let isolation_level = isolation_level.clone();
1261            let http = http.clone();
1262            async move { http.begin_transaction(&isolation_level, &token).await }
1263        })
1264        .await
1265    }
1266
1267    /// Get transaction status
1268    ///
1269    /// # Arguments
1270    ///
1271    /// * `transaction_id` - The transaction ID
1272    ///
1273    /// # Returns
1274    ///
1275    /// Transaction status including state and operations count
1276    pub async fn get_transaction_status(&self, transaction_id: &str) -> Result<serde_json::Value> {
1277        let transaction_id = transaction_id.to_string();
1278        let http = self.http.clone();
1279        self.execute_with_token_refresh(move |token| {
1280            let transaction_id = transaction_id.clone();
1281            let http = http.clone();
1282            async move { http.get_transaction_status(&transaction_id, &token).await }
1283        })
1284        .await
1285    }
1286
1287    /// Commit a transaction
1288    ///
1289    /// # Arguments
1290    ///
1291    /// * `transaction_id` - The transaction ID to commit
1292    pub async fn commit_transaction(&self, transaction_id: &str) -> Result<()> {
1293        let transaction_id = transaction_id.to_string();
1294        let http = self.http.clone();
1295        self.execute_with_token_refresh(move |token| {
1296            let transaction_id = transaction_id.clone();
1297            let http = http.clone();
1298            async move { http.commit_transaction(&transaction_id, &token).await }
1299        })
1300        .await
1301    }
1302
1303    /// Rollback a transaction, discarding all staged writes (nothing was applied).
1304    ///
1305    /// # Arguments
1306    ///
1307    /// * `transaction_id` - The transaction ID to rollback
1308    pub async fn rollback_transaction(&self, transaction_id: &str) -> Result<()> {
1309        let transaction_id = transaction_id.to_string();
1310        let http = self.http.clone();
1311        self.execute_with_token_refresh(move |token| {
1312            let transaction_id = transaction_id.clone();
1313            let http = http.clone();
1314            async move { http.rollback_transaction(&transaction_id, &token).await }
1315        })
1316        .await
1317    }
1318
1319    /// Create a savepoint within a transaction. A later
1320    /// [`rollback_to_savepoint`](Self::rollback_to_savepoint) discards everything
1321    /// staged after it.
1322    pub async fn create_savepoint(&self, transaction_id: &str, name: &str) -> Result<()> {
1323        let transaction_id = transaction_id.to_string();
1324        let name = name.to_string();
1325        let http = self.http.clone();
1326        self.execute_with_token_refresh(move |token| {
1327            let transaction_id = transaction_id.clone();
1328            let name = name.clone();
1329            let http = http.clone();
1330            async move { http.create_savepoint(&transaction_id, &name, &token).await }
1331        })
1332        .await
1333    }
1334
1335    /// Roll a transaction back to a savepoint, discarding writes staged after it.
1336    pub async fn rollback_to_savepoint(&self, transaction_id: &str, name: &str) -> Result<()> {
1337        let transaction_id = transaction_id.to_string();
1338        let name = name.to_string();
1339        let http = self.http.clone();
1340        self.execute_with_token_refresh(move |token| {
1341            let transaction_id = transaction_id.clone();
1342            let name = name.clone();
1343            let http = http.clone();
1344            async move {
1345                http.rollback_to_savepoint(&transaction_id, &name, &token)
1346                    .await
1347            }
1348        })
1349        .await
1350    }
1351
1352    /// Release (forget) a savepoint. Staged work is unaffected.
1353    pub async fn release_savepoint(&self, transaction_id: &str, name: &str) -> Result<()> {
1354        let transaction_id = transaction_id.to_string();
1355        let name = name.to_string();
1356        let http = self.http.clone();
1357        self.execute_with_token_refresh(move |token| {
1358            let transaction_id = transaction_id.clone();
1359            let name = name.clone();
1360            let http = http.clone();
1361            async move { http.release_savepoint(&transaction_id, &name, &token).await }
1362        })
1363        .await
1364    }
1365
1366    /// Find a record by ID within a transaction (read-your-writes): the read is
1367    /// served from the transaction's own view — its uncommitted staged writes,
1368    /// else the committed store — and recorded in its read set for commit-time
1369    /// conflict detection. Use the plain [`find_by_id`](Self::find_by_id) for an
1370    /// ordinary committed read.
1371    pub async fn find_by_id_in_transaction(
1372        &self,
1373        collection: &str,
1374        id: &str,
1375        transaction_id: &str,
1376        bypass_ripple: Option<bool>,
1377    ) -> Result<Record> {
1378        let collection = collection.to_string();
1379        let id = id.to_string();
1380        let transaction_id = transaction_id.to_string();
1381        let http = self.http.clone();
1382        self.execute_with_token_refresh(move |token| {
1383            let collection = collection.clone();
1384            let id = id.clone();
1385            let transaction_id = transaction_id.clone();
1386            let http = http.clone();
1387            async move {
1388                http.find_by_id_in_transaction(
1389                    &collection,
1390                    &id,
1391                    &transaction_id,
1392                    &token,
1393                    bypass_ripple,
1394                )
1395                .await
1396            }
1397        })
1398        .await
1399    }
1400
1401    /// Find records within a transaction (read-your-writes for the matched ids).
1402    pub async fn find_in_transaction(
1403        &self,
1404        collection: &str,
1405        query: Query,
1406        transaction_id: &str,
1407        bypass_ripple: Option<bool>,
1408    ) -> Result<Vec<Record>> {
1409        let collection = collection.to_string();
1410        let transaction_id = transaction_id.to_string();
1411        let http = self.http.clone();
1412        self.execute_with_token_refresh(move |token| {
1413            let collection = collection.clone();
1414            let query = query.clone();
1415            let transaction_id = transaction_id.clone();
1416            let http = http.clone();
1417            async move {
1418                http.find_in_transaction(&collection, query, &transaction_id, &token, bypass_ripple)
1419                    .await
1420            }
1421        })
1422        .await
1423    }
1424
1425    /// Connect to WebSocket endpoint
1426    ///
1427    /// # Arguments
1428    ///
1429    /// * `ws_url` - The WebSocket URL (e.g., "ws://localhost:8080/ws")
1430    ///
1431    /// # Returns
1432    ///
1433    /// A WebSocket client for real-time operations
1434    pub async fn websocket(&self, ws_url: &str) -> Result<crate::websocket::WebSocketClient> {
1435        let token = self.auth.get_token().await?;
1436        crate::websocket::WebSocketClient::new(ws_url, token)
1437    }
1438
1439    /// Perform a full-text search
1440    ///
1441    /// # Arguments
1442    ///
1443    /// * `collection` - The collection name
1444    /// * `search_query` - The search query with options
1445    ///
1446    /// # Returns
1447    ///
1448    /// Search results with scores and matched fields
1449    ///
1450    /// # Example
1451    ///
1452    /// ```no_run
1453    /// # use ekodb_client::{Client, SearchQuery};
1454    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1455    /// let query = SearchQuery::new("john doe")
1456    ///     .fields("name,email")
1457    ///     .fuzzy(true)
1458    ///     .min_score(0.5);
1459    ///
1460    /// let results = client.search("users", query).await?;
1461    /// println!("Found {} results", results.total);
1462    /// # Ok(())
1463    /// # }
1464    /// ```
1465    pub async fn search(
1466        &self,
1467        collection: &str,
1468        search_query: SearchQuery,
1469    ) -> Result<SearchResponse> {
1470        let collection = collection.to_string();
1471        let http = self.http.clone();
1472        self.execute_with_token_refresh(move |token| {
1473            let collection = collection.clone();
1474            let search_query = search_query.clone();
1475            let http = http.clone();
1476            async move { http.search(&collection, search_query, &token).await }
1477        })
1478        .await
1479    }
1480
1481    /// Get distinct (unique) values for a field across all records in a collection.
1482    ///
1483    /// Results are sorted alphabetically and deduplicated. Supports an optional filter
1484    /// to restrict which records are examined.
1485    ///
1486    /// # Arguments
1487    ///
1488    /// * `collection` - The collection name
1489    /// * `field` - The field to get distinct values for
1490    /// * `query` - Optional query with filter and bypass flags
1491    ///
1492    /// # Example
1493    ///
1494    /// ```no_run
1495    /// # use ekodb_client::{Client, DistinctValuesQuery};
1496    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1497    /// // Get all distinct statuses
1498    /// let resp = client.distinct_values("orders", "status", DistinctValuesQuery::new()).await?;
1499    /// println!("Statuses: {:?}", resp.values);
1500    ///
1501    /// // With filter
1502    /// use serde_json::json;
1503    /// let filter = json!({"type":"Condition","content":{"field":"active","operator":"Eq","value":true}});
1504    /// let resp = client.distinct_values("users", "role", DistinctValuesQuery::new().filter(filter)).await?;
1505    /// # Ok(())
1506    /// # }
1507    /// ```
1508    pub async fn distinct_values(
1509        &self,
1510        collection: &str,
1511        field: &str,
1512        query: DistinctValuesQuery,
1513    ) -> Result<DistinctValuesResponse> {
1514        let collection = collection.to_string();
1515        let field = field.to_string();
1516        let http = self.http.clone();
1517        self.execute_with_token_refresh(move |token| {
1518            let collection = collection.clone();
1519            let field = field.clone();
1520            let query = query.clone();
1521            let http = http.clone();
1522            async move {
1523                http.distinct_values(&collection, &field, query, &token)
1524                    .await
1525            }
1526        })
1527        .await
1528    }
1529
1530    /// Text-only search (full-text search)
1531    ///
1532    /// Convenience method for text search without vectors.
1533    ///
1534    /// # Arguments
1535    ///
1536    /// * `collection` - The collection to search
1537    /// * `query_text` - The search query text
1538    /// * `limit` - Maximum number of results
1539    ///
1540    /// # Example
1541    ///
1542    /// ```no_run
1543    /// # use ekodb_client::Client;
1544    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1545    /// let results = client.text_search("articles", "rust programming", 10).await?;
1546    /// # Ok(())
1547    /// # }
1548    /// ```
1549    pub async fn text_search(
1550        &self,
1551        collection: &str,
1552        query_text: &str,
1553        limit: usize,
1554    ) -> Result<Vec<Record>> {
1555        let search_query = SearchQuery::new(query_text).limit(limit);
1556        let response = self.search(collection, search_query).await?;
1557
1558        // Convert SearchResult to Record, injecting _score so callers can access it
1559        let records: Vec<Record> = response
1560            .results
1561            .into_iter()
1562            .filter_map(|result| {
1563                let score = result.score;
1564                let mut record: Record = serde_json::from_value(result.record).ok()?;
1565                record.insert("_score", score);
1566                Some(record)
1567            })
1568            .collect();
1569
1570        Ok(records)
1571    }
1572
1573    /// Hybrid search (combines text + vector search)
1574    ///
1575    /// Performs semantic search using both text and vector embeddings.
1576    ///
1577    /// # Arguments
1578    ///
1579    /// * `collection` - The collection to search
1580    /// * `query_text` - The search query text
1581    /// * `query_vector` - The query embedding vector
1582    /// * `limit` - Maximum number of results
1583    ///
1584    /// # Example
1585    ///
1586    /// ```no_run
1587    /// # use ekodb_client::Client;
1588    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1589    /// let embedding = vec![0.1, 0.2, 0.3]; // From embed() call
1590    /// let results = client.hybrid_search(
1591    ///     "articles",
1592    ///     "rust programming",
1593    ///     embedding,
1594    ///     10
1595    /// ).await?;
1596    /// # Ok(())
1597    /// # }
1598    /// ```
1599    pub async fn hybrid_search(
1600        &self,
1601        collection: &str,
1602        query_text: &str,
1603        query_vector: Vec<f64>,
1604        limit: usize,
1605    ) -> Result<Vec<Record>> {
1606        let search_query = SearchQuery::new(query_text)
1607            .vector(query_vector)
1608            .text_weight(0.5)
1609            .vector_weight(0.5)
1610            .limit(limit);
1611
1612        let response = self.search(collection, search_query).await?;
1613
1614        // Convert SearchResult to Record, injecting _score so callers can access it
1615        let records: Vec<Record> = response
1616            .results
1617            .into_iter()
1618            .filter_map(|result| {
1619                let score = result.score;
1620                let mut record: Record = serde_json::from_value(result.record).ok()?;
1621                record.insert("_score", score);
1622                Some(record)
1623            })
1624            .collect();
1625
1626        Ok(records)
1627    }
1628
1629    /// Find all records in a collection
1630    ///
1631    /// Convenience method to retrieve all records (up to a limit).
1632    ///
1633    /// # Arguments
1634    ///
1635    /// * `collection` - The collection name
1636    /// * `limit` - Maximum number of records to return
1637    ///
1638    /// # Example
1639    ///
1640    /// ```no_run
1641    /// # use ekodb_client::Client;
1642    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1643    /// let all_records = client.find_all("users", 100).await?;
1644    /// # Ok(())
1645    /// # }
1646    /// ```
1647    pub async fn find_all(&self, collection: &str, limit: usize) -> Result<Vec<Record>> {
1648        use crate::types::Query;
1649
1650        let query = Query {
1651            filter: None,
1652            sort: None,
1653            limit: Some(limit),
1654            skip: None,
1655            join: None,
1656            bypass_cache: None,
1657            bypass_ripple: None,
1658            select_fields: Some(Vec::new()),
1659            exclude_fields: Some(Vec::new()),
1660        };
1661
1662        self.find(collection, query, None).await
1663    }
1664
1665    /// Create a collection with schema
1666    ///
1667    /// # Arguments
1668    ///
1669    /// * `collection` - The collection name
1670    /// * `schema` - The schema definition
1671    ///
1672    /// # Example
1673    ///
1674    /// ```no_run
1675    /// # use ekodb_client::{Client, Schema, FieldTypeSchema};
1676    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
1677    /// let schema = Schema::new()
1678    ///     .add_field("name", FieldTypeSchema::new("string").required())
1679    ///     .add_field("email", FieldTypeSchema::new("string").unique())
1680    ///     .add_field("age", FieldTypeSchema::new("number"));
1681    ///
1682    /// client.create_collection("users", schema).await?;
1683    /// # Ok(())
1684    /// # }
1685    /// ```
1686    pub async fn create_collection(&self, collection: &str, schema: Schema) -> Result<()> {
1687        let collection = collection.to_string();
1688        let http = self.http.clone();
1689        self.execute_with_token_refresh(move |token| {
1690            let collection = collection.clone();
1691            let schema = schema.clone();
1692            let http = http.clone();
1693            async move { http.create_collection(&collection, schema, &token).await }
1694        })
1695        .await
1696    }
1697
1698    /// Get collection metadata and schema
1699    ///
1700    /// # Arguments
1701    ///
1702    /// * `collection` - The collection name
1703    ///
1704    /// # Returns
1705    ///
1706    /// Collection metadata including schema and analytics
1707    pub async fn get_collection(&self, collection: &str) -> Result<CollectionMetadata> {
1708        let collection = collection.to_string();
1709        let http = self.http.clone();
1710        self.execute_with_token_refresh(move |token| {
1711            let collection = collection.clone();
1712            let http = http.clone();
1713            async move { http.get_collection(&collection, &token).await }
1714        })
1715        .await
1716    }
1717
1718    /// Get collection schema
1719    ///
1720    /// # Arguments
1721    ///
1722    /// * `collection` - The collection name
1723    ///
1724    /// # Returns
1725    ///
1726    /// The collection schema
1727    pub async fn get_schema(&self, collection: &str) -> Result<Schema> {
1728        let collection = collection.to_string();
1729        let http = self.http.clone();
1730        self.execute_with_token_refresh(move |token| {
1731            let collection = collection.clone();
1732            let http = http.clone();
1733            async move { http.get_schema(&collection, &token).await }
1734        })
1735        .await
1736    }
1737
1738    // ========================================================================
1739    // Tool Dispatch
1740    // ========================================================================
1741
1742    /// Execute a tool via ekoDB's server-side tool pipeline.
1743    ///
1744    /// Calls `POST /api/chat/tools/execute` which goes through the same
1745    /// `execute_tool` function as the LLM tool-calling loop — with all
1746    /// collection filtering, permission enforcement, and internal collection
1747    /// blocking. No LLM round-trip.
1748    ///
1749    /// Returns `Some(Ok(result))` if the tool was executed,
1750    /// `Some(Err(e))` if execution failed, or `None` if the server doesn't
1751    /// support the endpoint (older ekoDB versions).
1752    pub async fn execute_tool(
1753        &self,
1754        tool_name: &str,
1755        params: &serde_json::Value,
1756        chat_id: Option<&str>,
1757    ) -> Option<Result<serde_json::Value>> {
1758        let tool_name = tool_name.to_string();
1759        let params = params.clone();
1760        let chat_id = chat_id.map(|s| s.to_string());
1761        let result = self
1762            .execute_with_token_refresh(|token| {
1763                let tool_name = tool_name.clone();
1764                let params = params.clone();
1765                let chat_id = chat_id.clone();
1766                async move {
1767                    self.http
1768                        .execute_tool_remote(&tool_name, &params, chat_id.as_deref(), &token)
1769                        .await
1770                }
1771            })
1772            .await;
1773        match result {
1774            Ok(result) => {
1775                // The server returns a ToolResult with success/result/error fields
1776                let success = result["success"].as_bool().unwrap_or(false);
1777                if success {
1778                    Some(Ok(result["result"].clone()))
1779                } else {
1780                    let error = result["error"]
1781                        .as_str()
1782                        .unwrap_or("tool execution failed")
1783                        .to_string();
1784                    Some(Err(crate::Error::ToolExecution(error)))
1785                }
1786            }
1787            // handle_response returns Error::NotFound for 404
1788            Err(crate::Error::NotFound) => None,
1789            // Also handle Error::Api with 405 (method not allowed)
1790            Err(crate::Error::Api { code: 405, .. }) => None,
1791            Err(e) => Some(Err(e)),
1792        }
1793    }
1794
1795    // ========================================================================
1796    // Chat Operations
1797    // ========================================================================
1798
1799    /// Get all available chat models
1800    ///
1801    /// # Returns
1802    ///
1803    /// List of available models from all providers
1804    pub async fn get_chat_models(&self) -> Result<crate::chat::Models> {
1805        let http = self.http.clone();
1806        self.execute_with_token_refresh(move |token| {
1807            let http = http.clone();
1808            async move { http.get_chat_models(&token).await }
1809        })
1810        .await
1811    }
1812
1813    /// Get all built-in server-side chat tool definitions.
1814    /// Returns a list of tool objects with `name`, `description`, and `parameters` fields.
1815    /// Used by planning agents to discover available tools dynamically.
1816    pub async fn get_chat_tools(&self) -> Result<Vec<serde_json::Value>> {
1817        let http = self.http.clone();
1818        self.execute_with_token_refresh(move |token| {
1819            let http = http.clone();
1820            async move { http.get_chat_tools(&token).await }
1821        })
1822        .await
1823    }
1824
1825    /// Get specific chat model information
1826    ///
1827    /// # Arguments
1828    ///
1829    /// * `model_name` - Name of the model provider (e.g., "openai", "anthropic")
1830    pub async fn get_chat_model(&self, model_name: &str) -> Result<Vec<String>> {
1831        let model_name = model_name.to_string();
1832        let http = self.http.clone();
1833        self.execute_with_token_refresh(move |token| {
1834            let model_name = model_name.clone();
1835            let http = http.clone();
1836            async move { http.get_chat_model(&model_name, &token).await }
1837        })
1838        .await
1839    }
1840
1841    /// Stateless raw LLM completion — no session, no history, no RAG.
1842    ///
1843    /// Sends a system prompt and user message directly to the LLM via ekoDB
1844    /// and returns the raw text response without any context injection or
1845    /// conversation management. Use this for structured-output tasks such as
1846    /// planning where the response must be parsed programmatically.
1847    ///
1848    /// This is the blocking HTTP variant. For deployed instances behind reverse
1849    /// proxies, prefer `raw_completion_stream()` (SSE) or use `WebSocketClient::raw_completion()`
1850    /// (WSS) to avoid proxy timeouts on long-running LLM calls.
1851    pub async fn raw_completion(
1852        &self,
1853        request: crate::chat::RawCompletionRequest,
1854    ) -> Result<crate::chat::RawCompletionResponse> {
1855        let http = self.http.clone();
1856        self.execute_with_token_refresh(move |token| {
1857            let request = request.clone();
1858            let http = http.clone();
1859            async move { http.raw_completion(request, &token).await }
1860        })
1861        .await
1862    }
1863
1864    /// Stateless raw LLM completion via SSE streaming.
1865    ///
1866    /// Same as `raw_completion()` but uses Server-Sent Events to keep the
1867    /// connection alive. Preferred for deployed instances where reverse proxies
1868    /// may kill idle HTTP connections before the LLM responds.
1869    pub async fn raw_completion_stream(
1870        &self,
1871        request: crate::chat::RawCompletionRequest,
1872    ) -> Result<crate::chat::RawCompletionResponse> {
1873        let http = self.http.clone();
1874        self.execute_with_token_refresh(move |token| {
1875            let request = request.clone();
1876            let http = http.clone();
1877            async move { http.raw_completion_stream(request, &token).await }
1878        })
1879        .await
1880    }
1881
1882    /// Stateless raw LLM completion via SSE with incremental token progress.
1883    ///
1884    /// Same as `raw_completion_stream()` but sends each token through the
1885    /// provided channel as it arrives, allowing callers to show real-time
1886    /// progress during long-running LLM calls (e.g., goal plan generation).
1887    pub async fn raw_completion_stream_with_progress(
1888        &self,
1889        request: crate::chat::RawCompletionRequest,
1890        progress_tx: tokio::sync::mpsc::Sender<String>,
1891    ) -> Result<crate::chat::RawCompletionResponse> {
1892        let token = self.auth.get_token().await?;
1893        self.http
1894            .raw_completion_stream_with_progress(request, &token, progress_tx)
1895            .await
1896    }
1897
1898    /// Create a new chat session
1899    ///
1900    /// # Arguments
1901    ///
1902    /// * `request` - The session creation request
1903    ///
1904    /// # Returns
1905    ///
1906    /// The created session information
1907    pub async fn create_chat_session(
1908        &self,
1909        request: crate::chat::CreateChatSessionRequest,
1910    ) -> Result<crate::chat::ChatResponse> {
1911        let http = self.http.clone();
1912        self.execute_with_token_refresh(move |token| {
1913            let request = request.clone();
1914            let http = http.clone();
1915            async move { http.create_chat_session(request, &token).await }
1916        })
1917        .await
1918    }
1919
1920    /// Get a chat session by ID
1921    ///
1922    /// # Arguments
1923    ///
1924    /// * `chat_id` - The session ID
1925    pub async fn get_chat_session(
1926        &self,
1927        chat_id: &str,
1928    ) -> Result<crate::chat::ChatSessionResponse> {
1929        let chat_id = chat_id.to_string();
1930        let http = self.http.clone();
1931        self.execute_with_token_refresh(move |token| {
1932            let chat_id = chat_id.clone();
1933            let http = http.clone();
1934            async move { http.get_chat_session(&chat_id, &token).await }
1935        })
1936        .await
1937    }
1938
1939    /// List all chat sessions
1940    ///
1941    /// # Arguments
1942    ///
1943    /// * `query` - Query parameters for pagination and sorting
1944    pub async fn list_chat_sessions(
1945        &self,
1946        query: crate::chat::ListSessionsQuery,
1947    ) -> Result<crate::chat::ListSessionsResponse> {
1948        let http = self.http.clone();
1949        self.execute_with_token_refresh(move |token| {
1950            let query = query.clone();
1951            let http = http.clone();
1952            async move { http.list_chat_sessions(query, &token).await }
1953        })
1954        .await
1955    }
1956
1957    /// Submit a client tool result for an in-flight SSE chat stream.
1958    pub async fn submit_chat_tool_result(
1959        &self,
1960        chat_id: &str,
1961        call_id: &str,
1962        success: bool,
1963        result: Option<serde_json::Value>,
1964        error: Option<String>,
1965    ) -> Result<()> {
1966        let chat_id = chat_id.to_string();
1967        let call_id = call_id.to_string();
1968        let http = self.http.clone();
1969        self.execute_with_token_refresh(move |token| {
1970            let chat_id = chat_id.clone();
1971            let call_id = call_id.clone();
1972            let result = result.clone();
1973            let error = error.clone();
1974            let http = http.clone();
1975            async move {
1976                http.submit_chat_tool_result(&chat_id, &call_id, success, result, error, &token)
1977                    .await
1978            }
1979        })
1980        .await
1981    }
1982
1983    /// Send a liveness keepalive for a pending client tool on an in-flight SSE
1984    /// chat stream. Not a result — it resets the server's per-tool wait
1985    /// deadline so a pending human confirmation or a long-running client tool
1986    /// doesn't get the turn timed out mid-response. Call periodically (well
1987    /// under the server's `client_tool_timeout_secs`, default 60s) while a
1988    /// confirmation prompt is shown or a tool is executing.
1989    pub async fn submit_chat_tool_keepalive(&self, chat_id: &str, call_id: &str) -> Result<()> {
1990        let chat_id = chat_id.to_string();
1991        let call_id = call_id.to_string();
1992        let http = self.http.clone();
1993        self.execute_with_token_refresh(move |token| {
1994            let chat_id = chat_id.clone();
1995            let call_id = call_id.clone();
1996            let http = http.clone();
1997            async move {
1998                http.submit_chat_tool_keepalive(&chat_id, &call_id, &token)
1999                    .await
2000            }
2001        })
2002        .await
2003    }
2004
2005    /// Update chat session metadata
2006    ///
2007    /// # Arguments
2008    ///
2009    /// * `chat_id` - The session ID
2010    /// * `request` - The update request
2011    pub async fn update_chat_session(
2012        &self,
2013        chat_id: &str,
2014        request: crate::chat::UpdateSessionRequest,
2015    ) -> Result<crate::chat::ChatSessionResponse> {
2016        let chat_id = chat_id.to_string();
2017        let http = self.http.clone();
2018        self.execute_with_token_refresh(move |token| {
2019            let chat_id = chat_id.clone();
2020            let request = request.clone();
2021            let http = http.clone();
2022            async move { http.update_chat_session(&chat_id, request, &token).await }
2023        })
2024        .await
2025    }
2026
2027    /// Delete a chat session
2028    ///
2029    /// # Arguments
2030    ///
2031    /// * `chat_id` - The session ID to delete
2032    pub async fn delete_chat_session(&self, chat_id: &str) -> Result<()> {
2033        let chat_id = chat_id.to_string();
2034        let http = self.http.clone();
2035        self.execute_with_token_refresh(move |token| {
2036            let chat_id = chat_id.clone();
2037            let http = http.clone();
2038            async move { http.delete_chat_session(&chat_id, &token).await }
2039        })
2040        .await
2041    }
2042
2043    /// Branch a chat session from an existing one
2044    ///
2045    /// # Arguments
2046    ///
2047    /// * `request` - The branch request with parent session info
2048    pub async fn branch_chat_session(
2049        &self,
2050        request: crate::chat::CreateChatSessionRequest,
2051    ) -> Result<crate::chat::ChatResponse> {
2052        let http = self.http.clone();
2053        self.execute_with_token_refresh(move |token| {
2054            let request = request.clone();
2055            let http = http.clone();
2056            async move { http.branch_chat_session(request, &token).await }
2057        })
2058        .await
2059    }
2060
2061    /// Merge multiple chat sessions
2062    ///
2063    /// # Arguments
2064    ///
2065    /// * `request` - The merge request with source and target sessions
2066    pub async fn merge_chat_sessions(
2067        &self,
2068        request: crate::chat::MergeSessionsRequest,
2069    ) -> Result<crate::chat::ChatSessionResponse> {
2070        let http = self.http.clone();
2071        self.execute_with_token_refresh(move |token| {
2072            let request = request.clone();
2073            let http = http.clone();
2074            async move { http.merge_chat_sessions(request, &token).await }
2075        })
2076        .await
2077    }
2078
2079    /// Send a message in an existing chat session
2080    ///
2081    /// # Arguments
2082    ///
2083    /// * `chat_id` - The session ID
2084    /// * `request` - The message request
2085    pub async fn chat_message(
2086        &self,
2087        chat_id: &str,
2088        request: crate::chat::ChatMessageRequest,
2089    ) -> Result<crate::chat::ChatResponse> {
2090        let chat_id = chat_id.to_string();
2091        let http = self.http.clone();
2092        self.execute_with_token_refresh(move |token| {
2093            let chat_id = chat_id.clone();
2094            let request = request.clone();
2095            let http = http.clone();
2096            async move { http.chat_message(&chat_id, request, &token).await }
2097        })
2098        .await
2099    }
2100
2101    /// Stream a chat message via SSE (Server-Sent Events).
2102    /// Returns a channel that yields `ChatStreamEvent` items as they arrive.
2103    /// Server-side tools execute normally; client-side tools are not supported over SSE.
2104    pub async fn chat_message_stream(
2105        &self,
2106        chat_id: &str,
2107        request: crate::chat::ChatMessageRequest,
2108    ) -> Result<tokio::sync::mpsc::Receiver<crate::websocket::ChatStreamEvent>> {
2109        let token = self.auth.get_token().await?;
2110        self.http
2111            .chat_message_stream(chat_id, request, &token)
2112            .await
2113    }
2114
2115    /// Get messages from a chat session
2116    ///
2117    /// # Arguments
2118    ///
2119    /// * `chat_id` - The session ID
2120    /// * `query` - Query parameters for pagination and sorting
2121    pub async fn get_chat_session_messages(
2122        &self,
2123        chat_id: &str,
2124        query: crate::chat::GetMessagesQuery,
2125    ) -> Result<crate::chat::GetMessagesResponse> {
2126        let chat_id = chat_id.to_string();
2127        let http = self.http.clone();
2128        self.execute_with_token_refresh(move |token| {
2129            let chat_id = chat_id.clone();
2130            let query = query.clone();
2131            let http = http.clone();
2132            async move {
2133                http.get_chat_session_messages(&chat_id, query, &token)
2134                    .await
2135            }
2136        })
2137        .await
2138    }
2139
2140    /// Get a specific message by ID
2141    ///
2142    /// # Arguments
2143    ///
2144    /// * `chat_id` - The session ID
2145    /// * `message_id` - The message ID
2146    pub async fn get_chat_message(&self, chat_id: &str, message_id: &str) -> Result<Record> {
2147        let chat_id = chat_id.to_string();
2148        let message_id = message_id.to_string();
2149        let http = self.http.clone();
2150        self.execute_with_token_refresh(move |token| {
2151            let chat_id = chat_id.clone();
2152            let message_id = message_id.clone();
2153            let http = http.clone();
2154            async move { http.get_chat_message(&chat_id, &message_id, &token).await }
2155        })
2156        .await
2157    }
2158
2159    /// Compact a chat session's history on demand.
2160    ///
2161    /// Folds the older messages into a single summary message and marks the
2162    /// originals "forgotten" so they stop being replayed — reclaiming
2163    /// context-window budget while keeping a faithful summary in the prompt.
2164    ///
2165    /// # Arguments
2166    ///
2167    /// * `chat_id` - The session ID
2168    /// * `keep_recent` - How many most-recent messages to keep verbatim.
2169    ///   `None` uses the session's `max_context_messages` (or 50). `Some(0)`
2170    ///   compacts the entire history.
2171    pub async fn compact_chat(
2172        &self,
2173        chat_id: &str,
2174        keep_recent: Option<usize>,
2175    ) -> Result<crate::chat::CompactChatResponse> {
2176        let chat_id = chat_id.to_string();
2177        let http = self.http.clone();
2178        self.execute_with_token_refresh(move |token| {
2179            let chat_id = chat_id.clone();
2180            let http = http.clone();
2181            async move {
2182                let request = crate::chat::CompactChatRequest { keep_recent };
2183                http.compact_chat_session(&chat_id, request, &token).await
2184            }
2185        })
2186        .await
2187    }
2188
2189    /// Generate embeddings for text using AI (via ekoDB Functions)
2190    ///
2191    /// Uses ekoDB's AI integration to generate vector embeddings for semantic search.
2192    /// Requires OPENAI_API_KEY to be set in the ekoDB server environment.
2193    ///
2194    /// # Arguments
2195    ///
2196    /// * `text` - The text to generate embeddings for
2197    /// * `model` - The embedding model to use (e.g., "text-embedding-3-small")
2198    ///
2199    /// # Returns
2200    ///
2201    /// A vector of f64 values representing the embedding
2202    ///
2203    /// # Example
2204    ///
2205    /// ```no_run
2206    /// # use ekodb_client::Client;
2207    /// # async fn example(client: &Client) -> Result<(), ekodb_client::Error> {
2208    /// let embedding = client.embed("Hello world", "text-embedding-3-small").await?;
2209    /// println!("Generated {} dimensions", embedding.len());
2210    /// # Ok(())
2211    /// # }
2212    /// ```
2213    pub async fn embed(&self, text: &str, model: &str) -> Result<Vec<f64>> {
2214        let text = text.to_string();
2215        let model = model.to_string();
2216        let http = self.http.clone();
2217        let response = self
2218            .execute_with_token_refresh(move |token| {
2219                let text = text.clone();
2220                let model = model.clone();
2221                let http = http.clone();
2222                async move {
2223                    let request = crate::chat::EmbedRequest {
2224                        text: Some(text),
2225                        texts: None,
2226                        model: Some(model),
2227                    };
2228                    http.embed(request, &token).await
2229                }
2230            })
2231            .await?;
2232        response
2233            .embeddings
2234            .into_iter()
2235            .next()
2236            .ok_or(crate::Error::Api {
2237                code: 500,
2238                message: "No embedding returned".to_string(),
2239            })
2240    }
2241
2242    /// Generate embeddings for multiple texts in a single batch request
2243    ///
2244    /// # Arguments
2245    ///
2246    /// * `texts` - The texts to generate embeddings for
2247    /// * `model` - The embedding model to use (e.g., "text-embedding-3-small")
2248    ///
2249    /// # Returns
2250    ///
2251    /// A vector of embedding vectors
2252    pub async fn embed_batch(&self, texts: Vec<String>, model: &str) -> Result<Vec<Vec<f64>>> {
2253        let model = model.to_string();
2254        let http = self.http.clone();
2255        let response = self
2256            .execute_with_token_refresh(move |token| {
2257                let texts = texts.clone();
2258                let model = model.clone();
2259                let http = http.clone();
2260                async move {
2261                    let request = crate::chat::EmbedRequest {
2262                        text: None,
2263                        texts: Some(texts),
2264                        model: Some(model),
2265                    };
2266                    http.embed(request, &token).await
2267                }
2268            })
2269            .await?;
2270        Ok(response.embeddings)
2271    }
2272
2273    /// Update a chat message
2274    ///
2275    /// # Arguments
2276    ///
2277    /// * `chat_id` - The session ID
2278    /// * `message_id` - The message ID
2279    /// * `request` - The update request
2280    pub async fn update_chat_message(
2281        &self,
2282        chat_id: &str,
2283        message_id: &str,
2284        request: crate::chat::UpdateMessageRequest,
2285    ) -> Result<Record> {
2286        let chat_id = chat_id.to_string();
2287        let message_id = message_id.to_string();
2288        let http = self.http.clone();
2289        self.execute_with_token_refresh(move |token| {
2290            let chat_id = chat_id.clone();
2291            let message_id = message_id.clone();
2292            let request = request.clone();
2293            let http = http.clone();
2294            async move {
2295                http.update_chat_message(&chat_id, &message_id, request, &token)
2296                    .await
2297            }
2298        })
2299        .await
2300    }
2301
2302    /// Delete a chat message
2303    ///
2304    /// # Arguments
2305    ///
2306    /// * `chat_id` - The session ID
2307    /// * `message_id` - The message ID to delete
2308    pub async fn delete_chat_message(&self, chat_id: &str, message_id: &str) -> Result<()> {
2309        let chat_id = chat_id.to_string();
2310        let message_id = message_id.to_string();
2311        let http = self.http.clone();
2312        self.execute_with_token_refresh(move |token| {
2313            let chat_id = chat_id.clone();
2314            let message_id = message_id.clone();
2315            let http = http.clone();
2316            async move {
2317                http.delete_chat_message(&chat_id, &message_id, &token)
2318                    .await
2319            }
2320        })
2321        .await
2322    }
2323
2324    /// Toggle message forgotten status
2325    ///
2326    /// # Arguments
2327    ///
2328    /// * `chat_id` - The session ID
2329    /// * `message_id` - The message ID
2330    /// * `request` - The toggle request
2331    pub async fn toggle_forgotten_message(
2332        &self,
2333        chat_id: &str,
2334        message_id: &str,
2335        request: crate::chat::ToggleForgottenRequest,
2336    ) -> Result<Record> {
2337        let chat_id = chat_id.to_string();
2338        let message_id = message_id.to_string();
2339        let http = self.http.clone();
2340        self.execute_with_token_refresh(move |token| {
2341            let chat_id = chat_id.clone();
2342            let message_id = message_id.clone();
2343            let request = request.clone();
2344            let http = http.clone();
2345            async move {
2346                http.toggle_forgotten_message(&chat_id, &message_id, request, &token)
2347                    .await
2348            }
2349        })
2350        .await
2351    }
2352
2353    /// Regenerate a chat message
2354    ///
2355    /// # Arguments
2356    ///
2357    /// * `chat_id` - The session ID
2358    /// * `message_id` - The message ID to regenerate
2359    pub async fn regenerate_chat_message(
2360        &self,
2361        chat_id: &str,
2362        message_id: &str,
2363    ) -> Result<crate::chat::ChatResponse> {
2364        let chat_id = chat_id.to_string();
2365        let message_id = message_id.to_string();
2366        let http = self.http.clone();
2367        self.execute_with_token_refresh(move |token| {
2368            let chat_id = chat_id.clone();
2369            let message_id = message_id.clone();
2370            let http = http.clone();
2371            async move {
2372                http.regenerate_chat_message(&chat_id, &message_id, &token)
2373                    .await
2374            }
2375        })
2376        .await
2377    }
2378
2379    /// Save a new function
2380    ///
2381    /// # Arguments
2382    ///
2383    /// * `function` - The UserFunction definition to save
2384    ///
2385    /// # Returns
2386    ///
2387    /// The UserFunction ID assigned by the server
2388    pub async fn save_function(&self, function: crate::functions::UserFunction) -> Result<String> {
2389        let http = self.http.clone();
2390        self.execute_with_token_refresh(move |token| {
2391            let function = function.clone();
2392            let http = http.clone();
2393            async move { http.save_function(function, &token).await }
2394        })
2395        .await
2396    }
2397
2398    /// Get a UserFunction by its ID
2399    ///
2400    /// # Arguments
2401    ///
2402    /// * `id` - The UserFunction ID (from save_function)
2403    ///
2404    /// # Returns
2405    ///
2406    /// The saved UserFunction definition
2407    pub async fn get_function(&self, id: &str) -> Result<crate::functions::UserFunction> {
2408        let id = id.to_string();
2409        let http = self.http.clone();
2410        self.execute_with_token_refresh(move |token| {
2411            let id = id.clone();
2412            let http = http.clone();
2413            async move { http.get_function(&id, &token).await }
2414        })
2415        .await
2416    }
2417
2418    /// List all saved functions, optionally filtered by tags
2419    ///
2420    /// # Arguments
2421    ///
2422    /// * `tags` - Optional list of tags to filter by
2423    ///
2424    /// # Returns
2425    ///
2426    /// Vector of saved functions
2427    pub async fn list_functions(
2428        &self,
2429        tags: Option<Vec<String>>,
2430    ) -> Result<Vec<crate::functions::UserFunction>> {
2431        let http = self.http.clone();
2432        self.execute_with_token_refresh(move |token| {
2433            let tags = tags.clone();
2434            let http = http.clone();
2435            async move { http.list_functions(tags, &token).await }
2436        })
2437        .await
2438    }
2439
2440    /// Update an existing function
2441    ///
2442    /// # Arguments
2443    ///
2444    /// * `id` - The UserFunction ID to update
2445    /// * `function` - The updated UserFunction definition
2446    pub async fn update_function(
2447        &self,
2448        id: &str,
2449        function: crate::functions::UserFunction,
2450    ) -> Result<()> {
2451        let id = id.to_string();
2452        let http = self.http.clone();
2453        self.execute_with_token_refresh(move |token| {
2454            let id = id.clone();
2455            let function = function.clone();
2456            let http = http.clone();
2457            async move { http.update_function(&id, function, &token).await }
2458        })
2459        .await
2460    }
2461
2462    /// Delete a UserFunction by its ID
2463    ///
2464    /// # Arguments
2465    ///
2466    /// * `id` - The UserFunction ID to delete
2467    pub async fn delete_function(&self, id: &str) -> Result<()> {
2468        let id = id.to_string();
2469        let http = self.http.clone();
2470        self.execute_with_token_refresh(move |token| {
2471            let id = id.clone();
2472            let http = http.clone();
2473            async move { http.delete_function(&id, &token).await }
2474        })
2475        .await
2476    }
2477
2478    /// Call a saved function
2479    ///
2480    /// # Arguments
2481    ///
2482    /// * `label` - The UserFunction label to execute
2483    /// * `params` - Optional parameters to pass to the function
2484    ///
2485    /// # Returns
2486    ///
2487    /// UserFunction execution result containing records and metadata
2488    ///
2489    /// # Example
2490    ///
2491    /// ```no_run
2492    /// # use ekodb_client::{Client, FieldType};
2493    /// # use std::collections::HashMap;
2494    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2495    /// let client = Client::builder()
2496    ///     .base_url("https://your-instance.ekodb.net")
2497    ///     .api_key("your-token")
2498    ///     .build()?;
2499    ///
2500    /// let mut params = HashMap::new();
2501    /// params.insert("status".to_string(), FieldType::String("active".to_string()));
2502    ///
2503    /// let result = client.call_function("get_active_users", Some(params)).await?;
2504    /// println!("Found {} records", result.records.len());
2505    /// # Ok(())
2506    /// # }
2507    /// ```
2508    pub async fn call_function(
2509        &self,
2510        function_id_or_label: &str,
2511        params: Option<std::collections::HashMap<String, crate::types::FieldType>>,
2512    ) -> Result<crate::functions::FunctionResult> {
2513        let function_id_or_label = function_id_or_label.to_string();
2514        let http = self.http.clone();
2515        self.execute_with_token_refresh(move |token| {
2516            let function_id_or_label = function_id_or_label.clone();
2517            let params = params.clone();
2518            let http = http.clone();
2519            async move {
2520                http.call_function(&function_id_or_label, params, &token)
2521                    .await
2522            }
2523        })
2524        .await
2525    }
2526
2527    // ========================================================================
2528    // USER FUNCTIONS API
2529    // ========================================================================
2530
2531    /// Save a new UserFunction
2532    ///
2533    /// # Arguments
2534    ///
2535    /// * `user_function` - The UserFunction definition to save
2536    ///
2537    /// # Returns
2538    ///
2539    /// The UserFunction ID assigned by the server
2540    pub async fn save_user_function(
2541        &self,
2542        user_function: crate::functions::UserFunction,
2543    ) -> Result<String> {
2544        let http = self.http.clone();
2545        self.execute_with_token_refresh(move |token| {
2546            let user_function = user_function.clone();
2547            let http = http.clone();
2548            async move { http.save_user_function(user_function, &token).await }
2549        })
2550        .await
2551    }
2552
2553    /// Get a UserFunction by its label
2554    ///
2555    /// # Arguments
2556    ///
2557    /// * `label` - The UserFunction label (unique identifier)
2558    ///
2559    /// # Returns
2560    ///
2561    /// The saved UserFunction definition
2562    pub async fn get_user_function(&self, label: &str) -> Result<crate::functions::UserFunction> {
2563        let label = label.to_string();
2564        let http = self.http.clone();
2565        self.execute_with_token_refresh(move |token| {
2566            let label = label.clone();
2567            let http = http.clone();
2568            async move { http.get_user_function(&label, &token).await }
2569        })
2570        .await
2571    }
2572
2573    /// List all saved UserFunctions, optionally filtered by tags
2574    ///
2575    /// # Arguments
2576    ///
2577    /// * `tags` - Optional list of tags to filter by
2578    ///
2579    /// # Returns
2580    ///
2581    /// Vector of saved UserFunctions
2582    pub async fn list_user_functions(
2583        &self,
2584        tags: Option<Vec<String>>,
2585    ) -> Result<Vec<crate::functions::UserFunction>> {
2586        let http = self.http.clone();
2587        self.execute_with_token_refresh(move |token| {
2588            let tags = tags.clone();
2589            let http = http.clone();
2590            async move { http.list_user_functions(tags, &token).await }
2591        })
2592        .await
2593    }
2594
2595    /// Update an existing UserFunction
2596    ///
2597    /// # Arguments
2598    ///
2599    /// * `label` - The UserFunction label to update
2600    /// * `user_function` - The updated UserFunction definition
2601    pub async fn update_user_function(
2602        &self,
2603        label: &str,
2604        user_function: crate::functions::UserFunction,
2605    ) -> Result<()> {
2606        let label = label.to_string();
2607        let http = self.http.clone();
2608        self.execute_with_token_refresh(move |token| {
2609            let label = label.clone();
2610            let user_function = user_function.clone();
2611            let http = http.clone();
2612            async move {
2613                http.update_user_function(&label, user_function, &token)
2614                    .await
2615            }
2616        })
2617        .await
2618    }
2619
2620    /// Delete a UserFunction by its label
2621    ///
2622    /// # Arguments
2623    ///
2624    /// * `label` - The UserFunction label to delete
2625    pub async fn delete_user_function(&self, label: &str) -> Result<()> {
2626        let label = label.to_string();
2627        let http = self.http.clone();
2628        self.execute_with_token_refresh(move |token| {
2629            let label = label.clone();
2630            let http = http.clone();
2631            async move { http.delete_user_function(&label, &token).await }
2632        })
2633        .await
2634    }
2635
2636    // ── Goal CRUD ────────────────────────────────────────────────────────────
2637
2638    pub async fn goal_create(&self, data: serde_json::Value) -> Result<serde_json::Value> {
2639        let http = self.http.clone();
2640        self.execute_with_token_refresh(move |token| {
2641            let data = data.clone();
2642            let http = http.clone();
2643            async move { http.goal_create(data, &token).await }
2644        })
2645        .await
2646    }
2647
2648    pub async fn goal_list(&self) -> Result<serde_json::Value> {
2649        let http = self.http.clone();
2650        self.execute_with_token_refresh(move |token| {
2651            let http = http.clone();
2652            async move { http.goal_list(&token).await }
2653        })
2654        .await
2655    }
2656
2657    pub async fn goal_get(&self, id: &str) -> Result<serde_json::Value> {
2658        let id = id.to_string();
2659        let http = self.http.clone();
2660        self.execute_with_token_refresh(move |token| {
2661            let id = id.clone();
2662            let http = http.clone();
2663            async move { http.goal_get(&id, &token).await }
2664        })
2665        .await
2666    }
2667
2668    pub async fn goal_update(
2669        &self,
2670        id: &str,
2671        data: serde_json::Value,
2672    ) -> Result<serde_json::Value> {
2673        let id = id.to_string();
2674        let http = self.http.clone();
2675        self.execute_with_token_refresh(move |token| {
2676            let id = id.clone();
2677            let data = data.clone();
2678            let http = http.clone();
2679            async move { http.goal_update(&id, data, &token).await }
2680        })
2681        .await
2682    }
2683
2684    pub async fn goal_delete(&self, id: &str) -> Result<()> {
2685        let id = id.to_string();
2686        let http = self.http.clone();
2687        self.execute_with_token_refresh(move |token| {
2688            let id = id.clone();
2689            let http = http.clone();
2690            async move { http.goal_delete(&id, &token).await }
2691        })
2692        .await
2693    }
2694
2695    pub async fn goal_search(&self, query: &str) -> Result<serde_json::Value> {
2696        let query = query.to_string();
2697        let http = self.http.clone();
2698        self.execute_with_token_refresh(move |token| {
2699            let query = query.clone();
2700            let http = http.clone();
2701            async move { http.goal_search(&query, &token).await }
2702        })
2703        .await
2704    }
2705
2706    // ── Goal lifecycle ─────────────────────────────────────────────────────
2707
2708    /// Atomically mark a goal as complete (status → pending_review).
2709    pub async fn goal_complete(
2710        &self,
2711        id: &str,
2712        data: serde_json::Value,
2713    ) -> Result<serde_json::Value> {
2714        let id = id.to_string();
2715        let http = self.http.clone();
2716        self.execute_with_token_refresh(move |token| {
2717            let id = id.clone();
2718            let data = data.clone();
2719            let http = http.clone();
2720            async move { http.goal_complete(&id, data, &token).await }
2721        })
2722        .await
2723    }
2724
2725    /// Atomically approve a goal (status → in_progress).
2726    pub async fn goal_approve(&self, id: &str) -> Result<serde_json::Value> {
2727        let id = id.to_string();
2728        let http = self.http.clone();
2729        self.execute_with_token_refresh(move |token| {
2730            let id = id.clone();
2731            let http = http.clone();
2732            async move { http.goal_approve(&id, &token).await }
2733        })
2734        .await
2735    }
2736
2737    /// Atomically reject a goal (status → failed).
2738    pub async fn goal_reject(
2739        &self,
2740        id: &str,
2741        data: serde_json::Value,
2742    ) -> Result<serde_json::Value> {
2743        let id = id.to_string();
2744        let http = self.http.clone();
2745        self.execute_with_token_refresh(move |token| {
2746            let id = id.clone();
2747            let data = data.clone();
2748            let http = http.clone();
2749            async move { http.goal_reject(&id, data, &token).await }
2750        })
2751        .await
2752    }
2753
2754    // ── Goal step lifecycle ──────────────────────────────────────────────────
2755
2756    /// Atomically mark a goal step as in_progress.
2757    pub async fn goal_step_start(&self, id: &str, step_index: usize) -> Result<serde_json::Value> {
2758        let id = id.to_string();
2759        let http = self.http.clone();
2760        self.execute_with_token_refresh(move |token| {
2761            let id = id.clone();
2762            let http = http.clone();
2763            async move { http.goal_step_start(&id, step_index, &token).await }
2764        })
2765        .await
2766    }
2767
2768    /// Atomically mark a goal step as completed with result.
2769    pub async fn goal_step_complete(
2770        &self,
2771        id: &str,
2772        step_index: usize,
2773        data: serde_json::Value,
2774    ) -> Result<serde_json::Value> {
2775        let id = id.to_string();
2776        let http = self.http.clone();
2777        self.execute_with_token_refresh(move |token| {
2778            let id = id.clone();
2779            let data = data.clone();
2780            let http = http.clone();
2781            async move { http.goal_step_complete(&id, step_index, data, &token).await }
2782        })
2783        .await
2784    }
2785
2786    /// Atomically mark a goal step as failed with error.
2787    pub async fn goal_step_fail(
2788        &self,
2789        id: &str,
2790        step_index: usize,
2791        data: serde_json::Value,
2792    ) -> Result<serde_json::Value> {
2793        let id = id.to_string();
2794        let http = self.http.clone();
2795        self.execute_with_token_refresh(move |token| {
2796            let id = id.clone();
2797            let data = data.clone();
2798            let http = http.clone();
2799            async move { http.goal_step_fail(&id, step_index, data, &token).await }
2800        })
2801        .await
2802    }
2803
2804    // ── Task CRUD ────────────────────────────────────────────────────────────
2805
2806    pub async fn task_create(&self, data: serde_json::Value) -> Result<serde_json::Value> {
2807        let http = self.http.clone();
2808        self.execute_with_token_refresh(move |token| {
2809            let data = data.clone();
2810            let http = http.clone();
2811            async move { http.task_create(data, &token).await }
2812        })
2813        .await
2814    }
2815
2816    pub async fn task_list(&self) -> Result<serde_json::Value> {
2817        let http = self.http.clone();
2818        self.execute_with_token_refresh(move |token| {
2819            let http = http.clone();
2820            async move { http.task_list(&token).await }
2821        })
2822        .await
2823    }
2824
2825    pub async fn task_get(&self, id: &str) -> Result<serde_json::Value> {
2826        let id = id.to_string();
2827        let http = self.http.clone();
2828        self.execute_with_token_refresh(move |token| {
2829            let id = id.clone();
2830            let http = http.clone();
2831            async move { http.task_get(&id, &token).await }
2832        })
2833        .await
2834    }
2835
2836    pub async fn task_update(
2837        &self,
2838        id: &str,
2839        data: serde_json::Value,
2840    ) -> Result<serde_json::Value> {
2841        let id = id.to_string();
2842        let http = self.http.clone();
2843        self.execute_with_token_refresh(move |token| {
2844            let id = id.clone();
2845            let data = data.clone();
2846            let http = http.clone();
2847            async move { http.task_update(&id, data, &token).await }
2848        })
2849        .await
2850    }
2851
2852    pub async fn task_delete(&self, id: &str) -> Result<()> {
2853        let id = id.to_string();
2854        let http = self.http.clone();
2855        self.execute_with_token_refresh(move |token| {
2856            let id = id.clone();
2857            let http = http.clone();
2858            async move { http.task_delete(&id, &token).await }
2859        })
2860        .await
2861    }
2862
2863    pub async fn task_due(&self, now: &str) -> Result<serde_json::Value> {
2864        let now = now.to_string();
2865        let http = self.http.clone();
2866        self.execute_with_token_refresh(move |token| {
2867            let now = now.clone();
2868            let http = http.clone();
2869            async move { http.task_due(&now, &token).await }
2870        })
2871        .await
2872    }
2873
2874    // ── Task lifecycle ──────────────────────────────────────────────────────
2875
2876    /// Atomically mark a task as running.
2877    pub async fn task_start(&self, id: &str) -> Result<serde_json::Value> {
2878        let id = id.to_string();
2879        let http = self.http.clone();
2880        self.execute_with_token_refresh(move |token| {
2881            let id = id.clone();
2882            let http = http.clone();
2883            async move { http.task_start(&id, &token).await }
2884        })
2885        .await
2886    }
2887
2888    /// Atomically mark a task as succeeded (increment run_count, reset failures).
2889    pub async fn task_succeed(
2890        &self,
2891        id: &str,
2892        data: serde_json::Value,
2893    ) -> Result<serde_json::Value> {
2894        let id = id.to_string();
2895        let http = self.http.clone();
2896        self.execute_with_token_refresh(move |token| {
2897            let id = id.clone();
2898            let data = data.clone();
2899            let http = http.clone();
2900            async move { http.task_succeed(&id, data, &token).await }
2901        })
2902        .await
2903    }
2904
2905    /// Atomically mark a task as failed (increment consecutive_failures).
2906    pub async fn task_fail(&self, id: &str, data: serde_json::Value) -> Result<serde_json::Value> {
2907        let id = id.to_string();
2908        let http = self.http.clone();
2909        self.execute_with_token_refresh(move |token| {
2910            let id = id.clone();
2911            let data = data.clone();
2912            let http = http.clone();
2913            async move { http.task_fail(&id, data, &token).await }
2914        })
2915        .await
2916    }
2917
2918    /// Atomically pause a task (status → paused).
2919    pub async fn task_pause(&self, id: &str) -> Result<serde_json::Value> {
2920        let id = id.to_string();
2921        let http = self.http.clone();
2922        self.execute_with_token_refresh(move |token| {
2923            let id = id.clone();
2924            let http = http.clone();
2925            async move { http.task_pause(&id, &token).await }
2926        })
2927        .await
2928    }
2929
2930    /// Atomically resume a task (status → active).
2931    pub async fn task_resume(
2932        &self,
2933        id: &str,
2934        data: serde_json::Value,
2935    ) -> Result<serde_json::Value> {
2936        let id = id.to_string();
2937        let http = self.http.clone();
2938        self.execute_with_token_refresh(move |token| {
2939            let id = id.clone();
2940            let data = data.clone();
2941            let http = http.clone();
2942            async move { http.task_resume(&id, data, &token).await }
2943        })
2944        .await
2945    }
2946
2947    // ── Agent CRUD ───────────────────────────────────────────────────────────
2948
2949    pub async fn agent_create(&self, data: serde_json::Value) -> Result<serde_json::Value> {
2950        let http = self.http.clone();
2951        self.execute_with_token_refresh(move |token| {
2952            let data = data.clone();
2953            let http = http.clone();
2954            async move { http.agent_create(data, &token).await }
2955        })
2956        .await
2957    }
2958
2959    pub async fn agent_list(&self) -> Result<serde_json::Value> {
2960        let http = self.http.clone();
2961        self.execute_with_token_refresh(move |token| {
2962            let http = http.clone();
2963            async move { http.agent_list(&token).await }
2964        })
2965        .await
2966    }
2967
2968    pub async fn agent_get(&self, id: &str) -> Result<serde_json::Value> {
2969        let id = id.to_string();
2970        let http = self.http.clone();
2971        self.execute_with_token_refresh(move |token| {
2972            let id = id.clone();
2973            let http = http.clone();
2974            async move { http.agent_get(&id, &token).await }
2975        })
2976        .await
2977    }
2978
2979    pub async fn agent_get_by_name(&self, name: &str) -> Result<serde_json::Value> {
2980        let name = name.to_string();
2981        let http = self.http.clone();
2982        self.execute_with_token_refresh(move |token| {
2983            let name = name.clone();
2984            let http = http.clone();
2985            async move { http.agent_get_by_name(&name, &token).await }
2986        })
2987        .await
2988    }
2989
2990    pub async fn agent_update(
2991        &self,
2992        id: &str,
2993        data: serde_json::Value,
2994    ) -> Result<serde_json::Value> {
2995        let id = id.to_string();
2996        let http = self.http.clone();
2997        self.execute_with_token_refresh(move |token| {
2998            let id = id.clone();
2999            let data = data.clone();
3000            let http = http.clone();
3001            async move { http.agent_update(&id, data, &token).await }
3002        })
3003        .await
3004    }
3005
3006    pub async fn agent_delete(&self, id: &str) -> Result<()> {
3007        let id = id.to_string();
3008        let http = self.http.clone();
3009        self.execute_with_token_refresh(move |token| {
3010            let id = id.clone();
3011            let http = http.clone();
3012            async move { http.agent_delete(&id, &token).await }
3013        })
3014        .await
3015    }
3016
3017    pub async fn agents_by_deployment(&self, deployment_id: &str) -> Result<serde_json::Value> {
3018        let deployment_id = deployment_id.to_string();
3019        let http = self.http.clone();
3020        self.execute_with_token_refresh(move |token| {
3021            let deployment_id = deployment_id.clone();
3022            let http = http.clone();
3023            async move { http.agents_by_deployment(&deployment_id, &token).await }
3024        })
3025        .await
3026    }
3027
3028    // ── Goal Template CRUD ──────────────────────────────────────────────────
3029
3030    pub async fn goal_template_create(&self, data: serde_json::Value) -> Result<serde_json::Value> {
3031        let http = self.http.clone();
3032        self.execute_with_token_refresh(move |token| {
3033            let data = data.clone();
3034            let http = http.clone();
3035            async move { http.goal_template_create(data, &token).await }
3036        })
3037        .await
3038    }
3039
3040    pub async fn goal_template_list(&self) -> Result<serde_json::Value> {
3041        let http = self.http.clone();
3042        self.execute_with_token_refresh(move |token| {
3043            let http = http.clone();
3044            async move { http.goal_template_list(&token).await }
3045        })
3046        .await
3047    }
3048
3049    pub async fn goal_template_get(&self, id: &str) -> Result<serde_json::Value> {
3050        let id = id.to_string();
3051        let http = self.http.clone();
3052        self.execute_with_token_refresh(move |token| {
3053            let id = id.clone();
3054            let http = http.clone();
3055            async move { http.goal_template_get(&id, &token).await }
3056        })
3057        .await
3058    }
3059
3060    pub async fn goal_template_update(
3061        &self,
3062        id: &str,
3063        data: serde_json::Value,
3064    ) -> Result<serde_json::Value> {
3065        let id = id.to_string();
3066        let http = self.http.clone();
3067        self.execute_with_token_refresh(move |token| {
3068            let id = id.clone();
3069            let data = data.clone();
3070            let http = http.clone();
3071            async move { http.goal_template_update(&id, data, &token).await }
3072        })
3073        .await
3074    }
3075
3076    pub async fn goal_template_delete(&self, id: &str) -> Result<()> {
3077        let id = id.to_string();
3078        let http = self.http.clone();
3079        self.execute_with_token_refresh(move |token| {
3080            let id = id.clone();
3081            let http = http.clone();
3082            async move { http.goal_template_delete(&id, &token).await }
3083        })
3084        .await
3085    }
3086
3087    // ── KV Document Linking ─────────────────────────────────────────────────
3088
3089    /// Get documents linked to a KV key.
3090    pub async fn kv_get_links(&self, key: &str) -> Result<serde_json::Value> {
3091        let key = key.to_string();
3092        let http = self.http.clone();
3093        self.execute_with_token_refresh(move |token| {
3094            let key = key.clone();
3095            let http = http.clone();
3096            async move { http.kv_get_links(&key, &token).await }
3097        })
3098        .await
3099    }
3100
3101    /// Link a document to a KV key.
3102    pub async fn kv_link(
3103        &self,
3104        key: &str,
3105        collection: &str,
3106        document_id: &str,
3107    ) -> Result<serde_json::Value> {
3108        let key = key.to_string();
3109        let collection = collection.to_string();
3110        let document_id = document_id.to_string();
3111        let http = self.http.clone();
3112        self.execute_with_token_refresh(move |token| {
3113            let key = key.clone();
3114            let collection = collection.clone();
3115            let document_id = document_id.clone();
3116            let http = http.clone();
3117            async move { http.kv_link(&key, &collection, &document_id, &token).await }
3118        })
3119        .await
3120    }
3121
3122    /// Unlink a document from a KV key.
3123    pub async fn kv_unlink(
3124        &self,
3125        key: &str,
3126        collection: &str,
3127        document_id: &str,
3128    ) -> Result<serde_json::Value> {
3129        let key = key.to_string();
3130        let collection = collection.to_string();
3131        let document_id = document_id.to_string();
3132        let http = self.http.clone();
3133        self.execute_with_token_refresh(move |token| {
3134            let key = key.clone();
3135            let collection = collection.clone();
3136            let document_id = document_id.clone();
3137            let http = http.clone();
3138            async move {
3139                http.kv_unlink(&key, &collection, &document_id, &token)
3140                    .await
3141            }
3142        })
3143        .await
3144    }
3145
3146    // ── Schedule Management ─────────────────────────────────────────────────
3147
3148    /// Create a new schedule.
3149    pub async fn create_schedule(&self, data: serde_json::Value) -> Result<serde_json::Value> {
3150        let http = self.http.clone();
3151        self.execute_with_token_refresh(move |token| {
3152            let data = data.clone();
3153            let http = http.clone();
3154            async move { http.create_schedule(data, &token).await }
3155        })
3156        .await
3157    }
3158
3159    /// List all schedules.
3160    pub async fn list_schedules(&self) -> Result<serde_json::Value> {
3161        let http = self.http.clone();
3162        self.execute_with_token_refresh(move |token| {
3163            let http = http.clone();
3164            async move { http.list_schedules(&token).await }
3165        })
3166        .await
3167    }
3168
3169    /// Get a schedule by ID.
3170    pub async fn get_schedule(&self, id: &str) -> Result<serde_json::Value> {
3171        let id = id.to_string();
3172        let http = self.http.clone();
3173        self.execute_with_token_refresh(move |token| {
3174            let id = id.clone();
3175            let http = http.clone();
3176            async move { http.get_schedule(&id, &token).await }
3177        })
3178        .await
3179    }
3180
3181    /// Update a schedule by ID.
3182    pub async fn update_schedule(
3183        &self,
3184        id: &str,
3185        data: serde_json::Value,
3186    ) -> Result<serde_json::Value> {
3187        let id = id.to_string();
3188        let http = self.http.clone();
3189        self.execute_with_token_refresh(move |token| {
3190            let id = id.clone();
3191            let data = data.clone();
3192            let http = http.clone();
3193            async move { http.update_schedule(&id, data, &token).await }
3194        })
3195        .await
3196    }
3197
3198    /// Delete a schedule by ID.
3199    pub async fn delete_schedule(&self, id: &str) -> Result<()> {
3200        let id = id.to_string();
3201        let http = self.http.clone();
3202        self.execute_with_token_refresh(move |token| {
3203            let id = id.clone();
3204            let http = http.clone();
3205            async move { http.delete_schedule(&id, &token).await }
3206        })
3207        .await
3208    }
3209
3210    /// Pause a schedule.
3211    pub async fn pause_schedule(&self, id: &str) -> Result<serde_json::Value> {
3212        let id = id.to_string();
3213        let http = self.http.clone();
3214        self.execute_with_token_refresh(move |token| {
3215            let id = id.clone();
3216            let http = http.clone();
3217            async move { http.pause_schedule(&id, &token).await }
3218        })
3219        .await
3220    }
3221
3222    /// Resume a schedule.
3223    pub async fn resume_schedule(&self, id: &str) -> Result<serde_json::Value> {
3224        let id = id.to_string();
3225        let http = self.http.clone();
3226        self.execute_with_token_refresh(move |token| {
3227            let id = id.clone();
3228            let http = http.clone();
3229            async move { http.resume_schedule(&id, &token).await }
3230        })
3231        .await
3232    }
3233
3234    // =========================================================================
3235    // Schema Cache & WebSocket Convenience
3236    // =========================================================================
3237
3238    /// Get a reference to the schema cache.
3239    pub fn schema_cache(&self) -> &crate::schema_cache::SchemaCache {
3240        &self.schema_cache
3241    }
3242
3243    /// Get the schema cache as an Arc (for sharing with WebSocketClient).
3244    pub fn schema_cache_arc(&self) -> Arc<crate::schema_cache::SchemaCache> {
3245        self.schema_cache.clone()
3246    }
3247
3248    /// Extract the record ID from a JSON record using the cached primary_key_alias.
3249    ///
3250    /// If the schema cache has the collection's alias, it's used first.
3251    /// Falls back to "id" then "_id" if the cache misses.
3252    pub fn extract_id(&self, collection: &str, record: &serde_json::Value) -> Option<String> {
3253        let alias = self.schema_cache.get_alias(collection);
3254        let extra: Vec<&str> = alias.iter().map(|s| s.as_str()).collect();
3255        crate::utils::extract_record_id(record, &extra)
3256    }
3257
3258    /// Subscribe to collection mutations via SSE (Server-Sent Events).
3259    ///
3260    /// Returns an `mpsc::Receiver` that yields `MutationNotificationPayload` events.
3261    /// Also handles `schema_changed` events to auto-invalidate the schema cache.
3262    ///
3263    /// Use this when WebSocket connections aren't available (e.g. behind reverse
3264    /// proxies that block WS upgrades).
3265    pub async fn subscribe_sse(
3266        &self,
3267        collection: &str,
3268        filter_field: Option<&str>,
3269        filter_value: Option<&str>,
3270    ) -> Result<tokio::sync::mpsc::Receiver<crate::websocket::MutationNotificationPayload>> {
3271        let token = self.auth.get_token().await?;
3272        let mut parsed_url =
3273            url::Url::parse(&format!("{}/api/subscribe/{}", self.base_url, collection)).map_err(
3274                |e| Error::Api {
3275                    code: 0,
3276                    message: format!("Invalid SSE URL: {}", e),
3277                },
3278            )?;
3279
3280        {
3281            let mut pairs = parsed_url.query_pairs_mut();
3282            if let Some(ff) = filter_field {
3283                pairs.append_pair("filter_field", ff);
3284            }
3285            if let Some(fv) = filter_value {
3286                pairs.append_pair("filter_value", fv);
3287            }
3288        }
3289
3290        let url = parsed_url.to_string();
3291
3292        let client = reqwest::Client::new();
3293        let response = client
3294            .get(&url)
3295            .header("Authorization", format!("Bearer {}", token))
3296            .header("Accept", "text/event-stream")
3297            .send()
3298            .await
3299            .map_err(|e| Error::Api {
3300                code: 0,
3301                message: format!("SSE connection failed: {}", e),
3302            })?;
3303
3304        if !response.status().is_success() {
3305            return Err(Error::Api {
3306                code: response.status().as_u16(),
3307                message: format!("SSE subscribe failed: {}", response.status()),
3308            });
3309        }
3310
3311        let (tx, rx) = tokio::sync::mpsc::channel(256);
3312        let schema_cache = self.schema_cache.clone();
3313
3314        // Spawn background task to parse SSE stream
3315        tokio::spawn(async move {
3316            use futures_util::StreamExt;
3317            let mut stream = response.bytes_stream();
3318            let mut buffer = String::new();
3319
3320            while let Some(chunk) = stream.next().await {
3321                let chunk = match chunk {
3322                    Ok(c) => c,
3323                    Err(_) => break,
3324                };
3325                let text = String::from_utf8_lossy(&chunk);
3326                // Normalize CRLF to LF for cross-platform SSE compatibility
3327                buffer.push_str(&text.replace("\r\n", "\n"));
3328
3329                // Parse SSE events from buffer (event: ...\ndata: ...\n\n)
3330                while let Some(end) = buffer.find("\n\n") {
3331                    let event_block = buffer[..end].to_owned();
3332                    buffer.drain(..end + 2);
3333
3334                    let mut event_type = String::new();
3335                    let mut data_lines: Vec<String> = Vec::new();
3336
3337                    for line in event_block.lines() {
3338                        if let Some(val) = line.strip_prefix("event: ") {
3339                            event_type = val.trim().to_string();
3340                        } else if let Some(val) = line.strip_prefix("data: ") {
3341                            data_lines.push(val.trim().to_string());
3342                        }
3343                    }
3344
3345                    let event_data = data_lines.join("\n");
3346
3347                    match event_type.as_str() {
3348                        "mutation" => {
3349                            if let Ok(payload) = serde_json::from_str::<
3350                                crate::websocket::MutationNotificationPayload,
3351                            >(&event_data)
3352                            {
3353                                if tx.send(payload).await.is_err() {
3354                                    return; // Receiver dropped
3355                                }
3356                            }
3357                        }
3358                        "schema_changed" => {
3359                            if let Ok(sc) = serde_json::from_str::<
3360                                crate::websocket::SchemaChangedPayload,
3361                            >(&event_data)
3362                            {
3363                                schema_cache.handle_schema_changed(
3364                                    &sc.collection,
3365                                    sc.version,
3366                                    &sc.primary_key_alias,
3367                                );
3368                            }
3369                        }
3370                        _ => {} // ignore subscribed, error, heartbeat
3371                    }
3372                }
3373            }
3374        });
3375
3376        Ok(rx)
3377    }
3378
3379    /// Create a WebSocket client connected to this ekoDB instance.
3380    ///
3381    /// Derives the WS URL from the base URL (http→ws, https→wss) and uses
3382    /// the current auth token. The schema cache is automatically attached
3383    /// for realtime invalidation on SchemaChanged events.
3384    pub async fn connect_ws(&self) -> Result<crate::websocket::WebSocketClient> {
3385        let token = self.auth.get_token().await?;
3386
3387        // Convert http(s) to ws(s)
3388        let ws_url = self
3389            .base_url
3390            .replace("https://", "wss://")
3391            .replace("http://", "ws://");
3392
3393        let ws = crate::websocket::WebSocketClient::new(&ws_url, token)?;
3394
3395        // Attach schema cache for auto-invalidation
3396        if self.schema_cache.is_enabled() {
3397            ws.set_schema_cache(self.schema_cache.clone()).await;
3398        }
3399
3400        Ok(ws)
3401    }
3402}
3403
3404/// Builder for creating a Client
3405#[derive(Default)]
3406pub struct ClientBuilder {
3407    base_url: Option<String>,
3408    api_key: Option<String>,
3409    timeout: Option<Duration>,
3410    max_retries: Option<usize>,
3411    should_retry: Option<bool>,
3412    serialization_format: Option<crate::types::SerializationFormat>,
3413    schema_cache_enabled: bool,
3414    schema_cache_ttl: Option<Duration>,
3415    schema_cache_max: Option<usize>,
3416}
3417
3418impl ClientBuilder {
3419    /// Create a new ClientBuilder
3420    pub fn new() -> Self {
3421        Self::default()
3422    }
3423
3424    /// Set the base URL for the ekoDB server
3425    ///
3426    /// # Example
3427    ///
3428    /// ```
3429    /// use ekodb_client::Client;
3430    ///
3431    /// let client = Client::builder()
3432    ///     .base_url("https://api.ekodb.net")
3433    ///     .api_key("your-api-key")
3434    ///     .build()?;
3435    /// # Ok::<(), ekodb_client::Error>(())
3436    /// ```
3437    pub fn base_url(mut self, url: impl Into<String>) -> Self {
3438        self.base_url = Some(url.into());
3439        self
3440    }
3441
3442    /// Set the API key for authentication
3443    ///
3444    /// The API key will be exchanged for a JWT token automatically.
3445    ///
3446    /// # Example
3447    ///
3448    /// ```
3449    /// use ekodb_client::Client;
3450    ///
3451    /// let client = Client::builder()
3452    ///     .base_url("https://api.ekodb.net")
3453    ///     .api_key("your-api-key")
3454    ///     .build()?;
3455    /// # Ok::<(), ekodb_client::Error>(())
3456    /// ```
3457    pub fn api_key(mut self, key: impl Into<String>) -> Self {
3458        self.api_key = Some(key.into());
3459        self
3460    }
3461
3462    /// Set the API token for authentication (alias for api_key for backward compatibility)
3463    #[deprecated(since = "0.1.0", note = "Use `api_key` instead")]
3464    pub fn api_token(mut self, token: impl Into<String>) -> Self {
3465        self.api_key = Some(token.into());
3466        self
3467    }
3468
3469    /// Set the request timeout
3470    ///
3471    /// Default: 30 seconds
3472    pub fn timeout(mut self, timeout: Duration) -> Self {
3473        self.timeout = Some(timeout);
3474        self
3475    }
3476
3477    /// Set the maximum number of retry attempts
3478    ///
3479    /// Default: 3
3480    pub fn max_retries(mut self, retries: usize) -> Self {
3481        self.max_retries = Some(retries);
3482        self
3483    }
3484
3485    /// Enable or disable automatic retries for rate limiting and transient errors
3486    ///
3487    /// When enabled (default), the client will automatically retry requests that fail
3488    /// due to rate limiting (429), service unavailable (503), timeouts, or connection errors.
3489    /// The retry delay respects the server's `Retry-After` header for rate limits.
3490    ///
3491    /// When disabled, all errors are returned immediately to the caller for manual handling.
3492    ///
3493    /// Default: true
3494    ///
3495    /// # Example
3496    ///
3497    /// ```
3498    /// use ekodb_client::Client;
3499    ///
3500    /// // Disable automatic retries
3501    /// let client = Client::builder()
3502    ///     .base_url("https://api.ekodb.net")
3503    ///     .api_key("your-api-key")
3504    ///     .should_retry(false)
3505    ///     .build()?;
3506    /// # Ok::<(), ekodb_client::Error>(())
3507    /// ```
3508    pub fn should_retry(mut self, should_retry: bool) -> Self {
3509        self.should_retry = Some(should_retry);
3510        self
3511    }
3512
3513    /// Set the serialization format for client-server communication
3514    ///
3515    /// Supports JSON (default, human-readable) and MessagePack (binary, faster).
3516    /// MessagePack can provide 2-3x performance improvement over JSON.
3517    ///
3518    /// Default: JSON
3519    ///
3520    /// # Example
3521    ///
3522    /// ```
3523    /// use ekodb_client::{Client, SerializationFormat};
3524    ///
3525    /// // Use MessagePack for better performance
3526    /// let client = Client::builder()
3527    ///     .base_url("https://api.ekodb.net")
3528    ///     .api_key("your-api-key")
3529    ///     .serialization_format(SerializationFormat::MessagePack)
3530    ///     .build()?;
3531    /// # Ok::<(), ekodb_client::Error>(())
3532    /// ```
3533    pub fn serialization_format(mut self, format: crate::types::SerializationFormat) -> Self {
3534        self.serialization_format = Some(format);
3535        self
3536    }
3537
3538    /// Enable the in-memory schema cache for primary_key_alias resolution.
3539    ///
3540    /// When enabled, CRUD results use cached schema metadata to correctly
3541    /// extract record IDs regardless of the collection's `primary_key_alias` config.
3542    /// The cache is invalidated automatically via WS `SchemaChanged` events.
3543    pub fn schema_cache(mut self, enabled: bool) -> Self {
3544        self.schema_cache_enabled = enabled;
3545        self
3546    }
3547
3548    /// Set the schema cache TTL (time-to-live) in seconds. Default: 300 (5 min).
3549    pub fn schema_cache_ttl(mut self, seconds: u64) -> Self {
3550        self.schema_cache_ttl = Some(Duration::from_secs(seconds));
3551        self
3552    }
3553
3554    /// Set the max number of collections the schema cache holds. Default: 100.
3555    pub fn schema_cache_max(mut self, max: usize) -> Self {
3556        self.schema_cache_max = Some(max);
3557        self
3558    }
3559
3560    /// Build the client
3561    ///
3562    /// # Errors
3563    ///
3564    /// Returns an error if required fields are missing or invalid
3565    pub fn build(self) -> Result<Client> {
3566        let base_url_str = self
3567            .base_url
3568            .ok_or_else(|| Error::InvalidConfig("base_url is required".to_string()))?;
3569
3570        let api_key = self
3571            .api_key
3572            .ok_or_else(|| Error::InvalidConfig("api_key is required".to_string()))?;
3573
3574        let timeout = self.timeout.unwrap_or(Duration::from_secs(30));
3575        let max_retries = self.max_retries.unwrap_or(3);
3576        let should_retry = self.should_retry.unwrap_or(true); // Default to true
3577        let format = self
3578            .serialization_format
3579            .unwrap_or(crate::types::SerializationFormat::MessagePack); // Default to MessagePack for 2-3x performance
3580
3581        // Parse base URL
3582        let base_url = url::Url::parse(&base_url_str)?;
3583
3584        // Create HTTP client with specified format
3585        let http = HttpClient::new(
3586            &base_url_str,
3587            timeout,
3588            max_retries as u32,
3589            should_retry,
3590            format,
3591        )?;
3592
3593        // Create reqwest client for auth
3594        let reqwest_client = reqwest::Client::builder().timeout(timeout).build()?;
3595
3596        // Create auth manager with API key
3597        let auth = AuthManager::new(api_key, base_url, reqwest_client);
3598
3599        let schema_cache =
3600            crate::schema_cache::SchemaCache::new(crate::schema_cache::SchemaCacheConfig {
3601                enabled: self.schema_cache_enabled,
3602                max_entries: self.schema_cache_max.unwrap_or(100),
3603                ttl: self.schema_cache_ttl.unwrap_or(Duration::from_secs(300)),
3604            });
3605
3606        Ok(Client {
3607            http: Arc::new(http),
3608            auth: Arc::new(auth),
3609            schema_cache: Arc::new(schema_cache),
3610            base_url: base_url_str,
3611        })
3612    }
3613}
3614
3615#[cfg(test)]
3616mod tests {
3617    use super::*;
3618
3619    #[test]
3620    fn test_client_builder_new() {
3621        let builder = ClientBuilder::new();
3622        assert!(builder.base_url.is_none());
3623        assert!(builder.api_key.is_none());
3624    }
3625
3626    #[test]
3627    fn test_client_builder_default() {
3628        let builder = ClientBuilder::default();
3629        assert!(builder.base_url.is_none());
3630        assert!(builder.api_key.is_none());
3631    }
3632
3633    #[test]
3634    fn test_client_builder_with_values() {
3635        let builder = ClientBuilder::new()
3636            .base_url("http://localhost:8080")
3637            .api_key("test-key")
3638            .timeout(Duration::from_secs(30))
3639            .max_retries(5);
3640
3641        assert_eq!(builder.base_url, Some("http://localhost:8080".to_string()));
3642        assert_eq!(builder.api_key, Some("test-key".to_string()));
3643        assert_eq!(builder.timeout, Some(Duration::from_secs(30)));
3644        assert_eq!(builder.max_retries, Some(5));
3645    }
3646
3647    #[test]
3648    fn test_client_builder_missing_base_url() {
3649        let result = ClientBuilder::new().api_key("test-key").build();
3650
3651        assert!(result.is_err());
3652        match result {
3653            Err(crate::Error::InvalidConfig(msg)) => {
3654                assert!(msg.contains("base_url"));
3655            }
3656            _ => panic!("Expected InvalidConfig error"),
3657        }
3658    }
3659
3660    #[test]
3661    fn test_client_builder_missing_api_key() {
3662        let result = ClientBuilder::new()
3663            .base_url("http://localhost:8080")
3664            .build();
3665
3666        assert!(result.is_err());
3667        match result {
3668            Err(crate::Error::InvalidConfig(msg)) => {
3669                assert!(msg.contains("api_key"));
3670            }
3671            _ => panic!("Expected InvalidConfig error"),
3672        }
3673    }
3674
3675    #[test]
3676    fn test_client_builder_invalid_url() {
3677        let result = ClientBuilder::new()
3678            .base_url("not-a-valid-url")
3679            .api_key("test-key")
3680            .build();
3681
3682        assert!(result.is_err());
3683    }
3684
3685    #[test]
3686    fn test_client_builder_valid() {
3687        let result = ClientBuilder::new()
3688            .base_url("http://localhost:8080")
3689            .api_key("test-key")
3690            .build();
3691
3692        assert!(result.is_ok());
3693    }
3694
3695    #[test]
3696    fn test_client_builder_with_custom_timeout() {
3697        let result = ClientBuilder::new()
3698            .base_url("http://localhost:8080")
3699            .api_key("test-key")
3700            .timeout(Duration::from_secs(60))
3701            .build();
3702
3703        assert!(result.is_ok());
3704    }
3705
3706    #[test]
3707    fn test_client_builder_with_custom_retries() {
3708        let result = ClientBuilder::new()
3709            .base_url("http://localhost:8080")
3710            .api_key("test-key")
3711            .max_retries(10)
3712            .build();
3713
3714        assert!(result.is_ok());
3715    }
3716
3717    #[test]
3718    fn test_client_builder_with_retry_enabled() {
3719        let result = ClientBuilder::new()
3720            .base_url("http://localhost:8080")
3721            .api_key("test-key")
3722            .should_retry(true)
3723            .build();
3724
3725        assert!(result.is_ok());
3726    }
3727
3728    #[test]
3729    fn test_client_builder_with_retry_disabled() {
3730        let result = ClientBuilder::new()
3731            .base_url("http://localhost:8080")
3732            .api_key("test-key")
3733            .should_retry(false)
3734            .build();
3735
3736        assert!(result.is_ok());
3737    }
3738
3739    #[test]
3740    fn test_client_builder_method() {
3741        let builder = Client::builder();
3742        assert!(builder.base_url.is_none());
3743    }
3744
3745    #[test]
3746    fn test_query_new() {
3747        let query = Query::new();
3748        assert!(query.filter.is_none());
3749        assert!(query.sort.is_none());
3750        assert!(query.limit.is_none());
3751        assert!(query.skip.is_none());
3752    }
3753
3754    #[test]
3755    fn test_query_with_filter() {
3756        let query = Query::new().filter(serde_json::json!({"name": "test"}));
3757        assert!(query.filter.is_some());
3758    }
3759
3760    #[test]
3761    fn test_query_with_sort() {
3762        let query = Query::new().sort(serde_json::json!({"created_at": -1}));
3763        assert!(query.sort.is_some());
3764    }
3765
3766    #[test]
3767    fn test_query_with_limit() {
3768        let query = Query::new().limit(10);
3769        assert_eq!(query.limit, Some(10));
3770    }
3771
3772    #[test]
3773    fn test_query_with_skip() {
3774        let query = Query::new().skip(20);
3775        assert_eq!(query.skip, Some(20));
3776    }
3777
3778    #[test]
3779    fn test_query_with_bypass_cache() {
3780        let query = Query::new().bypass_cache(true);
3781        assert_eq!(query.bypass_cache, Some(true));
3782    }
3783
3784    #[test]
3785    fn test_query_with_bypass_ripple() {
3786        let query = Query::new().bypass_ripple(true);
3787        assert_eq!(query.bypass_ripple, Some(true));
3788    }
3789
3790    #[test]
3791    fn test_query_with_join() {
3792        let join = serde_json::json!({
3793            "collections": ["users"],
3794            "local_field": "user_id",
3795            "foreign_field": "id",
3796            "as_field": "user"
3797        });
3798        let query = Query::new().join(join.clone());
3799        assert_eq!(query.join, Some(join));
3800    }
3801
3802    #[test]
3803    fn test_query_builder_chaining() {
3804        let query = Query::new()
3805            .filter(serde_json::json!({"status": "active"}))
3806            .sort(serde_json::json!({"created_at": -1}))
3807            .limit(10)
3808            .skip(20)
3809            .bypass_cache(true);
3810
3811        assert!(query.filter.is_some());
3812        assert!(query.sort.is_some());
3813        assert_eq!(query.limit, Some(10));
3814        assert_eq!(query.skip, Some(20));
3815        assert_eq!(query.bypass_cache, Some(true));
3816    }
3817
3818    #[test]
3819    fn test_record_new() {
3820        let record = Record::new();
3821        assert!(record.is_empty());
3822        assert_eq!(record.len(), 0);
3823    }
3824
3825    #[test]
3826    fn test_record_insert_and_get() {
3827        let mut record = Record::new();
3828        record.insert("name", "test");
3829
3830        assert!(!record.is_empty());
3831        assert_eq!(record.len(), 1);
3832        assert!(record.get("name").is_some());
3833    }
3834
3835    #[test]
3836    fn test_record_contains_key() {
3837        let mut record = Record::new();
3838        record.insert("name", "test");
3839
3840        assert!(record.contains_key("name"));
3841        assert!(!record.contains_key("age"));
3842    }
3843
3844    #[test]
3845    fn test_rate_limit_info_is_near_limit() {
3846        let info = RateLimitInfo {
3847            limit: 1000,
3848            remaining: 50, // 5% remaining
3849            reset: 1234567890,
3850        };
3851        assert!(info.is_near_limit());
3852
3853        let info2 = RateLimitInfo {
3854            limit: 1000,
3855            remaining: 500, // 50% remaining
3856            reset: 1234567890,
3857        };
3858        assert!(!info2.is_near_limit());
3859    }
3860
3861    #[test]
3862    fn test_rate_limit_info_is_exceeded() {
3863        let info = RateLimitInfo {
3864            limit: 1000,
3865            remaining: 0,
3866            reset: 1234567890,
3867        };
3868        assert!(info.is_exceeded());
3869
3870        let info2 = RateLimitInfo {
3871            limit: 1000,
3872            remaining: 1,
3873            reset: 1234567890,
3874        };
3875        assert!(!info2.is_exceeded());
3876    }
3877
3878    #[test]
3879    fn test_rate_limit_info_remaining_percentage() {
3880        let info = RateLimitInfo {
3881            limit: 1000,
3882            remaining: 250,
3883            reset: 1234567890,
3884        };
3885        assert_eq!(info.remaining_percentage(), 25.0);
3886
3887        let info2 = RateLimitInfo {
3888            limit: 1000,
3889            remaining: 0,
3890            reset: 1234567890,
3891        };
3892        assert_eq!(info2.remaining_percentage(), 0.0);
3893    }
3894
3895    #[test]
3896    fn test_record_remove() {
3897        let mut record = Record::new();
3898        record.insert("name", "test");
3899
3900        let removed = record.remove("name");
3901        assert!(removed.is_some());
3902        assert!(!record.contains_key("name"));
3903    }
3904
3905    /// Build a JWT-shaped token whose `exp` claim is far in the future so the
3906    /// auth manager treats the cached token as valid.
3907    fn far_future_jwt() -> String {
3908        use base64::Engine;
3909        let exp = std::time::SystemTime::now()
3910            .duration_since(std::time::UNIX_EPOCH)
3911            .unwrap()
3912            .as_secs()
3913            + 3600;
3914        let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
3915            .encode(serde_json::to_vec(&serde_json::json!({"typ":"JWT","alg":"HS256"})).unwrap());
3916        let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
3917            .encode(serde_json::to_vec(&serde_json::json!({"exp": exp, "sub":"test"})).unwrap());
3918        format!("{header}.{payload}.sig")
3919    }
3920
3921    /// End-to-end proof that a network method auto-refreshes on `TokenExpired`:
3922    /// the find endpoint returns 401 on the first call (→ `TokenExpired`),
3923    /// the client refreshes its token, and the retried call succeeds.
3924    #[tokio::test]
3925    async fn test_find_by_id_auto_refreshes_on_token_expired() {
3926        let mut server = mockito::Server::new_async().await;
3927        let jwt = far_future_jwt();
3928
3929        // Token endpoint: hit once for the initial get_token() and again for the
3930        // refresh_token() triggered by the 401.
3931        let token_mock = server
3932            .mock("POST", "/api/auth/token")
3933            .with_status(200)
3934            .with_header("content-type", "application/json")
3935            .with_body(serde_json::json!({ "token": jwt }).to_string())
3936            .expect(2)
3937            .create_async()
3938            .await;
3939
3940        // First find returns 401 → TokenExpired.
3941        let unauthorized = server
3942            .mock("GET", "/api/find/users/u1")
3943            .with_status(401)
3944            .with_header("content-type", "application/json")
3945            .with_body(r#"{"code":401,"message":"token expired"}"#)
3946            .expect(1)
3947            .create_async()
3948            .await;
3949
3950        // Retried find (after refresh) succeeds.
3951        let success = server
3952            .mock("GET", "/api/find/users/u1")
3953            .with_status(200)
3954            .with_header("content-type", "application/json")
3955            .with_body(r#"{"id":"u1","name":"Alice"}"#)
3956            .expect(1)
3957            .create_async()
3958            .await;
3959
3960        let client = Client::builder()
3961            .base_url(server.url())
3962            .api_key("test-key")
3963            .build()
3964            .expect("client builds");
3965
3966        let record = client
3967            .find_by_id("users", "u1", None)
3968            .await
3969            .expect("auto-refresh should make the retried call succeed");
3970
3971        assert_eq!(
3972            record.get("name").and_then(|v| v.as_string()),
3973            Some("Alice")
3974        );
3975
3976        // The token endpoint was hit twice (initial + refresh), the 401 once,
3977        // and the successful retry once — proving the refresh-and-retry path.
3978        token_mock.assert_async().await;
3979        unauthorized.assert_async().await;
3980        success.assert_async().await;
3981    }
3982}