1#![forbid(unsafe_code, clippy::unwrap_used)]
7#![allow(clippy::needless_return)]
8#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
9
10use eventsource_stream::Eventsource;
11pub use futures::Stream;
12use futures::StreamExt;
13use parking_lot::RwLock;
14use reqwest::header::{self, HeaderMap, HeaderValue};
15use reqwest::{Method, StatusCode};
16use std::borrow::Cow;
17use std::sync::Arc;
18use thiserror::Error;
19use tracing::*;
20
21use serde::de::DeserializeOwned;
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Error)]
26#[non_exhaustive]
27pub enum Error {
28 #[error("HTTP status: {0}")]
29 HttpStatus(StatusCode),
30
31 #[error("MissingRefreshToken")]
32 MissingRefreshToken,
33
34 #[error("RecordSerialization: {0}")]
35 RecordSerialization(serde_json::Error),
36
37 #[error("InvalidToken: {0}")]
38 InvalidToken(jsonwebtoken::errors::Error),
39
40 #[error("InvalidUrl: {0}")]
41 InvalidUrl(url::ParseError),
42
43 #[error("Reqwest: {0}")]
45 OtherReqwest(reqwest::Error),
46}
47
48impl From<reqwest::Error> for Error {
49 fn from(err: reqwest::Error) -> Self {
50 match err.status() {
51 Some(code) => Self::HttpStatus(code),
52 _ => Self::OtherReqwest(err),
53 }
54 }
55}
56
57#[derive(Clone, Debug)]
59pub struct User {
60 pub sub: String,
61 pub email: String,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
68pub struct Tokens {
69 pub auth_token: String,
70 pub refresh_token: Option<String>,
71 pub csrf_token: Option<String>,
72}
73
74#[derive(Clone, Debug, Default, PartialEq)]
75pub struct Pagination {
76 cursor: Option<String>,
77 limit: Option<usize>,
78 offset: Option<usize>,
79}
80
81impl Pagination {
82 pub fn new() -> Self {
83 return Self::default();
84 }
85
86 pub fn with_limit(mut self, limit: impl Into<Option<usize>>) -> Pagination {
87 self.limit = limit.into();
88 return self;
89 }
90
91 pub fn with_cursor(mut self, cursor: impl Into<Option<String>>) -> Pagination {
92 self.cursor = cursor.into();
93 return self;
94 }
95
96 pub fn with_offset(mut self, offset: impl Into<Option<usize>>) -> Pagination {
97 self.offset = offset.into();
98 return self;
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub enum DbEvent {
104 Update(Option<serde_json::Value>),
105 Insert(Option<serde_json::Value>),
106 Delete(Option<serde_json::Value>),
107 Error(String),
108}
109
110#[derive(Clone, Debug, Deserialize)]
111pub struct ListResponse<T> {
112 pub cursor: Option<String>,
113 pub total_count: Option<usize>,
114 pub records: Vec<T>,
115}
116
117pub trait RecordId<'a> {
118 fn serialized_id(self) -> Cow<'a, str>;
119}
120
121impl RecordId<'_> for String {
122 fn serialized_id(self) -> Cow<'static, str> {
123 return Cow::Owned(self);
124 }
125}
126
127impl<'a> RecordId<'a> for &'a String {
128 fn serialized_id(self) -> Cow<'a, str> {
129 return Cow::Borrowed(self);
130 }
131}
132
133impl<'a> RecordId<'a> for &'a str {
134 fn serialized_id(self) -> Cow<'a, str> {
135 return Cow::Borrowed(self);
136 }
137}
138
139impl RecordId<'_> for i64 {
140 fn serialized_id(self) -> Cow<'static, str> {
141 return Cow::Owned(self.to_string());
142 }
143}
144
145pub trait ReadArgumentsTrait<'a> {
146 fn serialized_id(self) -> Cow<'a, str>;
147 fn expand(&self) -> Option<&Vec<&'a str>>;
148}
149
150impl<'a, T: RecordId<'a>> ReadArgumentsTrait<'a> for T {
151 fn serialized_id(self) -> Cow<'a, str> {
152 return self.serialized_id();
153 }
154
155 fn expand(&self) -> Option<&Vec<&'a str>> {
156 return None;
157 }
158}
159
160#[derive(Clone, Debug, PartialEq)]
161pub struct ReadArguments<'a, T: RecordId<'a>> {
162 id: T,
163 expand: Option<Vec<&'a str>>,
164}
165
166impl<'a, T: RecordId<'a>> ReadArguments<'a, T> {
167 pub fn new(id: T) -> Self {
168 return Self { id, expand: None };
169 }
170
171 pub fn with_expand(mut self, expand: impl AsRef<[&'a str]>) -> Self {
172 self.expand = Some(expand.as_ref().to_vec());
173 return self;
174 }
175}
176
177impl<'a, T: RecordId<'a>> ReadArgumentsTrait<'a> for ReadArguments<'a, T> {
178 fn serialized_id(self) -> Cow<'a, str> {
179 return self.id.serialized_id();
180 }
181
182 fn expand(&self) -> Option<&Vec<&'a str>> {
183 return self.expand.as_ref();
184 }
185}
186
187struct ThinClient {
188 client: reqwest::Client,
189 url: url::Url,
190}
191
192impl ThinClient {
193 async fn fetch<T: Serialize>(
194 &self,
195 path: &str,
196 headers: HeaderMap,
197 method: Method,
198 body: Option<&T>,
199 query_params: Option<&[(Cow<'static, str>, Cow<'static, str>)]>,
200 ) -> Result<reqwest::Response, Error> {
201 assert!(path.starts_with("/"));
202
203 let mut url = self.url.clone();
204 url.set_path(path);
205
206 if let Some(query_params) = query_params {
207 let mut params = url.query_pairs_mut();
208 for (key, value) in query_params {
209 params.append_pair(key, value);
210 }
211 }
212
213 let request = {
214 let mut builder = self.client.request(method, url).headers(headers);
215 if let Some(ref body) = body {
216 let json = serde_json::to_string(body).map_err(Error::RecordSerialization)?;
217 builder = builder.body(json);
218 }
219 builder.build()?
220 };
221
222 return Ok(self.client.execute(request).await?);
223 }
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
227struct JwtTokenClaims {
228 sub: String,
229 iat: i64,
230 exp: i64,
231 email: String,
232 csrf_token: String,
233}
234
235fn decode_auth_token<T: DeserializeOwned>(token: &str) -> Result<T, Error> {
236 let decoding_key = jsonwebtoken::DecodingKey::from_secret(&[]);
237
238 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
240 validation.insecure_disable_signature_validation();
241
242 return jsonwebtoken::decode::<T>(token, &decoding_key, &validation)
243 .map(|data| data.claims)
244 .map_err(Error::InvalidToken);
245}
246
247#[derive(Clone)]
248pub struct RecordApi {
249 client: Arc<ClientState>,
250 name: String,
251}
252
253#[derive(Clone, Debug, Default, PartialEq)]
254pub struct ListArguments<'a> {
255 pagination: Pagination,
256 order: Option<Vec<&'a str>>,
257 filters: Option<ValueOrFilterGroup>,
258 expand: Option<Vec<&'a str>>,
259 count: bool,
260}
261
262#[derive(Clone, Copy, Debug, PartialEq)]
263pub enum CompareOp {
264 Equal,
265 NotEqual,
266 GreaterThanEqual,
267 GreaterThan,
268 LessThanEqual,
269 LessThan,
270 Like,
271 Regexp,
272}
273
274impl CompareOp {
275 fn format(&self) -> &'static str {
276 return match self {
277 Self::Equal => "$eq",
278 Self::NotEqual => "$ne",
279 Self::GreaterThanEqual => "$gte",
280 Self::GreaterThan => "$gt",
281 Self::LessThanEqual => "$lte",
282 Self::LessThan => "$lt",
283 Self::Like => "$like",
284 Self::Regexp => "$re",
285 };
286 }
287}
288
289#[derive(Clone, Default, Debug, PartialEq)]
290pub struct Filter {
291 pub column: String,
292 pub op: Option<CompareOp>,
293 pub value: String,
294}
295
296impl Filter {
297 pub fn new(column: impl Into<String>, op: CompareOp, value: impl Into<String>) -> Self {
298 return Self {
299 column: column.into(),
300 op: Some(op),
301 value: value.into(),
302 };
303 }
304}
305
306impl From<Filter> for ValueOrFilterGroup {
307 fn from(value: Filter) -> Self {
308 return ValueOrFilterGroup::Filter(value);
309 }
310}
311
312#[derive(Clone, Debug, PartialEq)]
313pub enum ValueOrFilterGroup {
314 Filter(Filter),
315 And(Vec<ValueOrFilterGroup>),
316 Or(Vec<ValueOrFilterGroup>),
317}
318
319impl<F> From<F> for ValueOrFilterGroup
320where
321 F: Into<Vec<Filter>>,
322{
323 fn from(filters: F) -> Self {
324 return ValueOrFilterGroup::And(
325 filters
326 .into()
327 .into_iter()
328 .map(ValueOrFilterGroup::Filter)
329 .collect(),
330 );
331 }
332}
333
334impl<'a> ListArguments<'a> {
335 pub fn new() -> Self {
336 return ListArguments::default();
337 }
338
339 pub fn with_pagination(mut self, pagination: Pagination) -> Self {
340 self.pagination = pagination;
341 return self;
342 }
343
344 pub fn with_order(mut self, order: impl AsRef<[&'a str]>) -> Self {
345 self.order = Some(order.as_ref().to_vec());
346 return self;
347 }
348
349 pub fn with_filters(mut self, filters: impl Into<ValueOrFilterGroup>) -> Self {
350 self.filters = Some(filters.into());
351 return self;
352 }
353
354 pub fn with_expand(mut self, expand: impl AsRef<[&'a str]>) -> Self {
355 self.expand = Some(expand.as_ref().to_vec());
356 return self;
357 }
358
359 pub fn with_count(mut self, count: bool) -> Self {
360 self.count = count;
361 return self;
362 }
363}
364
365impl RecordApi {
366 pub async fn list<T: DeserializeOwned>(
367 &self,
368 args: ListArguments<'_>,
369 ) -> Result<ListResponse<T>, Error> {
370 type Param = (Cow<'static, str>, Cow<'static, str>);
371 let mut params: Vec<Param> = vec![];
372 if let Some(cursor) = args.pagination.cursor {
373 params.push((Cow::Borrowed("cursor"), Cow::Owned(cursor)));
374 }
375
376 if let Some(limit) = args.pagination.limit {
377 params.push((Cow::Borrowed("limit"), Cow::Owned(limit.to_string())));
378 }
379
380 #[inline]
381 fn to_list(slice: &[&str]) -> String {
382 return slice.join(",");
383 }
384
385 if let Some(order) = args.order {
386 if !order.is_empty() {
387 params.push((Cow::Borrowed("order"), Cow::Owned(to_list(&order))));
388 }
389 }
390
391 if let Some(expand) = args.expand {
392 if !expand.is_empty() {
393 params.push((Cow::Borrowed("expand"), Cow::Owned(to_list(&expand))));
394 }
395 }
396
397 if args.count {
398 params.push((Cow::Borrowed("count"), Cow::Borrowed("true")));
399 }
400
401 fn traverse_filters(params: &mut Vec<Param>, path: String, filter: ValueOrFilterGroup) {
402 match filter {
403 ValueOrFilterGroup::Filter(filter) => {
404 if let Some(op) = filter.op {
405 params.push((
406 Cow::Owned(format!(
407 "{path}[{col}][{op}]",
408 col = filter.column,
409 op = op.format()
410 )),
411 Cow::Owned(filter.value),
412 ));
413 } else {
414 params.push((
415 Cow::Owned(format!("{path}[{col}]", col = filter.column)),
416 Cow::Owned(filter.value),
417 ));
418 }
419 }
420 ValueOrFilterGroup::And(vec) => {
421 for (i, f) in vec.into_iter().enumerate() {
422 traverse_filters(params, format!("{path}[$and][{i}]"), f);
423 }
424 }
425 ValueOrFilterGroup::Or(vec) => {
426 for (i, f) in vec.into_iter().enumerate() {
427 traverse_filters(params, format!("{path}[$or][{i}]"), f);
428 }
429 }
430 }
431 }
432
433 if let Some(filters) = args.filters {
434 traverse_filters(&mut params, "filter".to_string(), filters);
435 }
436
437 let response = self
438 .client
439 .fetch(
440 &format!("/{RECORD_API}/{}", self.name),
441 Method::GET,
442 None::<&()>,
443 Some(¶ms),
444 )
445 .await?;
446
447 return json(response).await;
448 }
449
450 pub async fn read<'a, T: DeserializeOwned>(
451 &self,
452 args: impl ReadArgumentsTrait<'a>,
453 ) -> Result<T, Error> {
454 let expand = args
455 .expand()
456 .map(|e| vec![(Cow::Borrowed("expand"), Cow::Owned(e.join(",")))]);
457
458 let response = self
459 .client
460 .fetch(
461 &format!(
462 "/{RECORD_API}/{name}/{id}",
463 name = self.name,
464 id = args.serialized_id()
465 ),
466 Method::GET,
467 None::<&()>,
468 expand.as_deref(),
469 )
470 .await?;
471
472 return json(response).await;
473 }
474
475 pub async fn create<T: Serialize>(&self, record: T) -> Result<String, Error> {
476 return Ok(self.create_impl(record).await?.swap_remove(0));
477 }
478
479 pub async fn create_bulk<T: Serialize>(&self, record: &[T]) -> Result<Vec<String>, Error> {
480 return self.create_impl(record).await;
481 }
482
483 async fn create_impl<T: Serialize>(&self, record: T) -> Result<Vec<String>, Error> {
484 let response = self
485 .client
486 .fetch(
487 &format!("/{RECORD_API}/{name}", name = self.name),
488 Method::POST,
489 Some(&record),
490 None,
491 )
492 .await?;
493
494 #[derive(Deserialize)]
495 pub struct RecordIdResponse {
496 pub ids: Vec<String>,
497 }
498
499 return Ok(json::<RecordIdResponse>(response).await?.ids);
500 }
501
502 pub async fn update<'a, T: Serialize>(
503 &self,
504 id: impl RecordId<'a>,
505 record: T,
506 ) -> Result<(), Error> {
507 self
508 .client
509 .fetch(
510 &format!(
511 "/{RECORD_API}/{name}/{id}",
512 name = self.name,
513 id = id.serialized_id()
514 ),
515 Method::PATCH,
516 Some(&record),
517 None,
518 )
519 .await?;
520
521 return Ok(());
522 }
523
524 pub async fn delete<'a>(&self, id: impl RecordId<'a>) -> Result<(), Error> {
525 self
526 .client
527 .fetch(
528 &format!(
529 "/{RECORD_API}/{name}/{id}",
530 name = self.name,
531 id = id.serialized_id()
532 ),
533 Method::DELETE,
534 None::<&()>,
535 None,
536 )
537 .await?;
538
539 return Ok(());
540 }
541
542 pub async fn subscribe<'a, T: RecordId<'a>>(
543 &self,
544 id: T,
545 ) -> Result<impl Stream<Item = DbEvent> + use<T>, Error> {
546 let response = self
548 .client
549 .fetch(
550 &format!(
551 "/{RECORD_API}/{name}/subscribe/{id}",
552 name = self.name,
553 id = id.serialized_id()
554 ),
555 Method::GET,
556 None::<&()>,
557 None,
558 )
559 .await?;
560
561 return Ok(
562 response
563 .bytes_stream()
564 .eventsource()
565 .filter_map(|event_or| async {
566 if let Ok(event) = event_or {
567 if let Ok(db_event) = serde_json::from_str::<DbEvent>(&event.data) {
568 return Some(db_event);
569 }
570 }
571 return None;
572 }),
573 );
574 }
575}
576
577#[derive(Clone, Debug)]
578struct TokenState {
579 state: Option<(Tokens, JwtTokenClaims)>,
580 headers: HeaderMap,
581}
582
583impl TokenState {
584 fn build(tokens: Option<&Tokens>) -> TokenState {
585 let headers = build_headers(tokens);
586 return TokenState {
587 state: tokens.and_then(|tokens| {
588 let Ok(jwt_token) = decode_auth_token::<JwtTokenClaims>(&tokens.auth_token) else {
589 error!("Failed to decode auth token.");
590 return None;
591 };
592 return Some((tokens.clone(), jwt_token));
593 }),
594 headers,
595 };
596 }
597}
598
599struct ClientState {
600 client: ThinClient,
601 site: String,
602 tokens: RwLock<TokenState>,
603}
604
605impl ClientState {
606 #[inline]
607 async fn fetch<T: Serialize>(
608 &self,
609 path: &str,
610 method: Method,
611 body: Option<&T>,
612 query_params: Option<&[(Cow<'static, str>, Cow<'static, str>)]>,
613 ) -> Result<reqwest::Response, Error> {
614 let (mut headers, refresh_token) = self.extract_headers_and_refresh_token_if_exp();
615 if let Some(refresh_token) = refresh_token {
616 let new_tokens = ClientState::refresh_tokens(&self.client, headers, refresh_token).await?;
617
618 headers = new_tokens.headers.clone();
619 *self.tokens.write() = new_tokens;
620 }
621
622 return Ok(
623 self
624 .client
625 .fetch(path, headers, method, body, query_params)
626 .await?
627 .error_for_status()?,
628 );
629 }
630
631 #[inline]
632 fn extract_headers_and_refresh_token_if_exp(&self) -> (HeaderMap, Option<String>) {
633 #[inline]
634 fn should_refresh(jwt: &JwtTokenClaims) -> bool {
635 return jwt.exp - 60 < now() as i64;
636 }
637
638 let tokens = self.tokens.read();
639 let headers = tokens.headers.clone();
640 return match tokens.state {
641 Some(ref state) if should_refresh(&state.1) => (headers, state.0.refresh_token.clone()),
642 _ => (headers, None),
643 };
644 }
645
646 fn extract_headers_refresh_token(&self) -> Option<(HeaderMap, String)> {
647 let tokens = self.tokens.read();
648 let state = tokens.state.as_ref()?;
649
650 if let Some(ref refresh_token) = state.0.refresh_token {
651 return Some((tokens.headers.clone(), refresh_token.clone()));
652 }
653 return None;
654 }
655
656 async fn refresh_tokens(
657 client: &ThinClient,
658 headers: HeaderMap,
659 refresh_token: String,
660 ) -> Result<TokenState, Error> {
661 #[derive(Serialize)]
662 struct RefreshRequest<'a> {
663 refresh_token: &'a str,
664 }
665
666 let response = client
667 .fetch(
668 &format!("/{AUTH_API}/refresh"),
669 headers,
670 Method::POST,
671 Some(&RefreshRequest {
672 refresh_token: &refresh_token,
673 }),
674 None,
675 )
676 .await?;
677
678 #[derive(Deserialize)]
679 struct RefreshResponse {
680 auth_token: String,
681 csrf_token: Option<String>,
682 }
683
684 let refresh_response: RefreshResponse = json(response).await?;
685 return Ok(TokenState::build(Some(&Tokens {
686 auth_token: refresh_response.auth_token,
687 refresh_token: Some(refresh_token),
688 csrf_token: refresh_response.csrf_token,
689 })));
690 }
691}
692
693#[derive(Clone)]
694pub struct Client {
695 state: Arc<ClientState>,
696}
697
698impl Client {
699 pub fn new(site: &str, tokens: Option<Tokens>) -> Result<Client, Error> {
700 return Ok(Client {
701 state: Arc::new(ClientState {
702 client: ThinClient {
703 client: reqwest::Client::new(),
704 url: url::Url::parse(site).map_err(Error::InvalidUrl)?,
705 },
706 site: site.to_string(),
707 tokens: RwLock::new(TokenState::build(tokens.as_ref())),
708 }),
709 });
710 }
711
712 pub fn site(&self) -> String {
713 return self.state.site.clone();
714 }
715
716 pub fn tokens(&self) -> Option<Tokens> {
717 return self.state.tokens.read().state.as_ref().map(|x| x.0.clone());
718 }
719
720 pub fn user(&self) -> Option<User> {
721 if let Some(state) = &self.state.tokens.read().state {
722 return Some(User {
723 sub: state.1.sub.clone(),
724 email: state.1.email.clone(),
725 });
726 }
727 return None;
728 }
729
730 pub fn records(&self, api_name: &str) -> RecordApi {
731 return RecordApi {
732 client: self.state.clone(),
733 name: api_name.to_string(),
734 };
735 }
736
737 pub async fn refresh(&self) -> Result<(), Error> {
738 let Some((headers, refresh_token)) = self.state.extract_headers_refresh_token() else {
739 return Err(Error::MissingRefreshToken);
740 };
741
742 let new_tokens =
743 ClientState::refresh_tokens(&self.state.client, headers, refresh_token).await?;
744
745 *self.state.tokens.write() = new_tokens;
746 return Ok(());
747 }
748
749 pub async fn login(&self, email: &str, password: &str) -> Result<Tokens, Error> {
750 #[derive(Serialize)]
751 struct Credentials<'a> {
752 email: &'a str,
753 password: &'a str,
754 }
755
756 let response = self
757 .state
758 .fetch(
759 &format!("/{AUTH_API}/login"),
760 Method::POST,
761 Some(&Credentials { email, password }),
762 None,
763 )
764 .await?;
765
766 let tokens: Tokens = json(response).await?;
767 self.update_tokens(Some(&tokens));
768 return Ok(tokens);
769 }
770
771 pub async fn logout(&self) -> Result<(), Error> {
772 #[derive(Serialize)]
773 struct LogoutRequest {
774 refresh_token: String,
775 }
776
777 let response_or = match self.state.extract_headers_refresh_token() {
778 Some((_headers, refresh_token)) => {
779 self
780 .state
781 .fetch(
782 &format!("/{AUTH_API}/logout"),
783 Method::POST,
784 Some(&LogoutRequest { refresh_token }),
785 None,
786 )
787 .await
788 }
789 _ => {
790 self
791 .state
792 .fetch(
793 &format!("/{AUTH_API}/logout"),
794 Method::GET,
795 None::<&()>,
796 None,
797 )
798 .await
799 }
800 };
801
802 self.update_tokens(None);
803
804 return response_or.map(|_| ());
805 }
806
807 fn update_tokens(&self, tokens: Option<&Tokens>) -> TokenState {
808 let state = TokenState::build(tokens);
809
810 *self.state.tokens.write() = state.clone();
811 if let Some(ref s) = state.state {
814 let now = now();
815 if s.1.exp < now as i64 {
816 warn!("Token expired");
817 }
818 }
819
820 return state;
821 }
822}
823
824fn build_headers(tokens: Option<&Tokens>) -> HeaderMap {
825 let mut base = HeaderMap::with_capacity(5);
826 base.insert(
827 header::CONTENT_TYPE,
828 HeaderValue::from_static("application/json"),
829 );
830
831 if let Some(tokens) = tokens {
832 if let Ok(value) = HeaderValue::from_str(&format!("Bearer {}", tokens.auth_token)) {
833 base.insert(header::AUTHORIZATION, value);
834 } else {
835 error!("Failed to build bearer token.");
836 }
837
838 if let Some(ref refresh) = tokens.refresh_token {
839 if let Ok(value) = HeaderValue::from_str(refresh) {
840 base.insert("Refresh-Token", value);
841 } else {
842 error!("Failed to build refresh token header.");
843 }
844 }
845
846 if let Some(ref csrf) = tokens.csrf_token {
847 if let Ok(value) = HeaderValue::from_str(csrf) {
848 base.insert("CSRF-Token", value);
849 } else {
850 error!("Failed to build refresh token header.");
851 }
852 }
853 }
854
855 return base;
856}
857
858fn now() -> u64 {
859 return std::time::SystemTime::now()
860 .duration_since(std::time::UNIX_EPOCH)
861 .expect("Duration since epoch")
862 .as_secs();
863}
864
865#[inline]
866async fn json<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
867 let full = resp.bytes().await?;
868 return serde_json::from_slice(&full).map_err(Error::RecordSerialization);
869}
870
871const AUTH_API: &str = "api/auth/v1";
872const RECORD_API: &str = "api/records/v1";
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 #[tokio::test]
879 async fn is_send_test() {
880 let client = Client::new("http://127.0.0.1:4000", None).unwrap();
881
882 let api = client.records("simple_strict_table");
883
884 for _ in 0..2 {
885 let api = api.clone();
886 tokio::spawn(async move {
887 let response = api.read::<serde_json::Value>(0).await;
889 assert!(response.is_err());
890 })
891 .await
892 .unwrap();
893 }
894 }
895}