add get_record()

This commit is contained in:
Brent Schroeter 2025-10-19 01:14:29 -07:00
parent 17e698547b
commit 0614425125
4 changed files with 116 additions and 17 deletions

View file

@ -1,3 +1,5 @@
use std::error::Error;
use ferrtable::Client; use ferrtable::Client;
use futures::StreamExt as _; use futures::StreamExt as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,14 +27,14 @@ enum RecordStatus {
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<(), Box<dyn Error>> {
let settings = Settings::load().unwrap(); let settings = Settings::load()?;
let client = Client::new_from_access_token(&settings.access_token).unwrap(); let client = Client::new_from_access_token(&settings.access_token)?;
println!("Testing Client::list_bases()..."); println!("Testing Client::list_bases()...");
let mut bases = client.list_bases().build().unwrap().stream_items(); let mut bases = client.list_bases().build()?.stream_items();
while let Some(res) = bases.next().await { while let Some(res) = bases.next().await {
dbg!(res.unwrap()); dbg!(res?);
} }
println!("Testing Client::create_records()..."); println!("Testing Client::create_records()...");
@ -44,28 +46,46 @@ async fn main() {
}]) }])
.with_base_id(settings.base_id.clone()) .with_base_id(settings.base_id.clone())
.with_table_id(settings.table_id.clone()) .with_table_id(settings.table_id.clone())
.build() .build()?
.unwrap()
.execute() .execute()
.await .await?;
.unwrap();
println!("Testing Client::list_records()..."); println!("Testing Client::list_records()...");
let records = client let records = client
.list_records() .list_records()
.with_base_id(settings.base_id.clone()) .with_base_id(settings.base_id.clone())
.with_table_id(settings.table_id.clone()) .with_table_id(settings.table_id.clone())
.build() .build()?
.unwrap()
.stream_items::<TestRecord>() .stream_items::<TestRecord>()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await .await
.into_iter() .into_iter()
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()?;
.unwrap(); for rec in records.iter() {
for rec in records { dbg!(&rec.fields);
dbg!(rec.fields);
} }
let record = client
.get_record()
.with_base_id(settings.base_id.clone())
.with_table_id(settings.table_id.clone())
.with_record_id("does_not_exist".to_string())
.build()?
.fetch_optional::<TestRecord>()
.await?;
assert!(record.is_none());
let record = client
.get_record()
.with_base_id(settings.base_id.clone())
.with_table_id(settings.table_id.clone())
.with_record_id(records.first().unwrap().id.clone())
.build()?
.fetch_optional::<TestRecord>()
.await?;
dbg!(record);
println!("All tests succeeded."); println!("All tests succeeded.");
Ok(())
} }

View file

@ -5,8 +5,8 @@ use std::fmt::Debug;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
create_records::CreateRecordsQueryBuilder, list_bases::ListBasesQueryBuilder, create_records::CreateRecordsQueryBuilder, get_record::GetRecordQueryBuilder,
list_records::ListRecordsQueryBuilder, list_bases::ListBasesQueryBuilder, list_records::ListRecordsQueryBuilder,
}; };
const DEFAULT_API_ROOT: &str = "https://api.airtable.com"; const DEFAULT_API_ROOT: &str = "https://api.airtable.com";
@ -68,6 +68,32 @@ impl Client {
.with_records(records.into_iter().collect()) .with_records(records.into_iter().collect())
} }
/// Retrieve a single record. Any "empty" fields (e.g. "", [], or false) in
/// the record will not be returned.
///
/// Note If we can't locate the record on a given table, the request will
/// fallback to a base wide search and will still return the record if the
/// Record ID is valid and the record is located within the same base.
///
/// # Examples
///
/// ## Basic Usage
///
/// ```no_run
/// let result = client
/// .get_record()
/// .with_base_id("***".to_owned())
/// .with_table_id("***".to_owned())
/// .with_record_id("***".to_owned())
/// .build()?
/// .fetch_optional()
/// .await?;
/// dbg!(result);
/// ```
pub fn get_record(&self) -> GetRecordQueryBuilder {
GetRecordQueryBuilder::default().with_client(self.clone())
}
/// List the bases the token can access /// List the bases the token can access
/// ///
/// # Examples /// # Examples

View file

@ -0,0 +1,52 @@
use std::fmt::Debug;
use derive_builder::Builder;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::{Serialize, de::DeserializeOwned};
use crate::client::Client;
use crate::errors::ExecutionError;
use crate::types::AirtableRecord;
#[derive(Builder, Clone, Debug, Serialize)]
#[builder(pattern = "owned", setter(prefix = "with"))]
pub struct GetRecordQuery {
#[serde(skip)]
base_id: String,
#[serde(skip)]
#[builder(vis = "pub(crate)")]
client: Client,
#[serde(skip)]
record_id: String,
#[serde(skip)]
table_id: String,
}
impl GetRecordQuery {
pub async fn fetch_optional<T>(self) -> Result<Option<AirtableRecord<T>>, ExecutionError>
where
T: Clone + Debug + DeserializeOwned,
{
let base_id = utf8_percent_encode(&self.base_id, NON_ALPHANUMERIC).to_string();
let table_id = utf8_percent_encode(&self.table_id, NON_ALPHANUMERIC).to_string();
let record_id = utf8_percent_encode(&self.record_id, NON_ALPHANUMERIC).to_string();
let http_resp = self
.client
.get_path(&format!("v0/{base_id}/{table_id}/{record_id}"))
.send()
.await?;
match http_resp.error_for_status() {
Ok(http_resp) => Ok(http_resp.json().await?),
Err(err) => {
if err.status() == Some(reqwest::StatusCode::NOT_FOUND) {
Ok(None)
} else {
Err(err.into())
}
}
}
}
}

View file

@ -91,6 +91,7 @@ pub mod types;
// Each API operation is organized into a dedicated Rust module. // Each API operation is organized into a dedicated Rust module.
pub mod create_records; pub mod create_records;
pub mod get_record;
pub mod list_bases; pub mod list_bases;
pub mod list_records; pub mod list_records;