use std::collections::HashMap;
use std::convert::TryFrom;
use prost_types::value::Kind;
use tonic::Request;
use tonic::body::Body;
use tonic::codegen::{Bytes, StdError};
use crate::anytype::ClientCommandsClient;
use crate::anytype::rpc::object::show::Request as ObjectShowRequest;
use crate::auth::with_token;
pub use crate::error::ViewError;
use crate::model;
use crate::model::block::ContentValue;
use crate::model::block::content::Dataview as BlockDataview;
use crate::model::block::content::dataview;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GridViewColumn {
pub relation_key: String,
pub name: String,
pub format: Option<model::RelationFormat>,
pub formula: dataview::relation::FormulaType,
pub is_visible: bool,
pub width: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GridViewInfo {
pub block_id: String,
pub view_id: String,
pub view_name: String,
pub columns: Vec<GridViewColumn>,
}
pub async fn fetch_grid_view_columns<S>(
client: &mut ClientCommandsClient<S>,
token: &str,
space_id: &str,
type_id: &str,
view_id: &str,
) -> Result<GridViewInfo, ViewError>
where
S: tonic::client::GrpcService<Body>,
S::Error: Into<StdError>,
S::ResponseBody: http_body::Body<Data = Bytes> + Send + 'static,
<S::ResponseBody as http_body::Body>::Error: Into<StdError> + Send,
{
let request = ObjectShowRequest {
object_id: type_id.to_string(),
space_id: space_id.to_string(),
include_relations_as_dependent_objects: true,
..Default::default()
};
let request = authenticated_request(request, token)?;
let response = client.object_show(request).await?.into_inner();
if let Some(error) = response.error
&& error.code != 0
{
return Err(ViewError::ApiResponse {
code: error.code,
description: error.description,
});
}
let object_view = response.object_view.ok_or(ViewError::MissingObjectView)?;
let relation_names = relation_name_index(&object_view.details);
let (block_id, dataview) = find_dataview_block(&object_view.blocks, view_id)?;
let view = dataview
.views
.iter()
.find(|view| view.id == view_id)
.ok_or_else(|| ViewError::MissingView {
view_id: view_id.to_string(),
})?;
let view_type =
dataview::view::Type::try_from(view.r#type).unwrap_or(dataview::view::Type::Table);
if view_type != dataview::view::Type::Table && view_type != dataview::view::Type::List {
return Err(ViewError::NotSupportedView {
view_id: view_id.to_string(),
actual: view.r#type,
});
}
let relation_formats = relation_format_index(&dataview);
let columns = view
.relations
.iter()
.map(|relation| {
let formula = dataview::relation::FormulaType::try_from(relation.formula)
.unwrap_or(dataview::relation::FormulaType::None);
let name = relation_names
.get(&relation.key)
.cloned()
.unwrap_or_else(|| relation.key.clone());
let format = relation_formats.get(&relation.key).cloned();
GridViewColumn {
relation_key: relation.key.clone(),
name,
format,
formula,
is_visible: relation.is_visible,
width: relation.width,
}
})
.collect::<Vec<_>>();
Ok(GridViewInfo {
block_id,
view_id: view.id.clone(),
view_name: view.name.clone(),
columns,
})
}
fn authenticated_request<T>(message: T, token: &str) -> Result<Request<T>, ViewError> {
with_token(Request::new(message), token).map_err(ViewError::from)
}
fn find_dataview_block(
blocks: &[model::Block],
view_id: &str,
) -> Result<(String, BlockDataview), ViewError> {
for block in blocks {
if let Some(ContentValue::Dataview(dataview)) = block.content_value.as_ref()
&& dataview.views.iter().any(|view| view.id == view_id)
{
return Ok((block.id.clone(), dataview.clone()));
}
}
Err(ViewError::MissingDataviewBlock {
view_id: view_id.to_string(),
})
}
fn relation_format_index(dataview: &BlockDataview) -> HashMap<String, model::RelationFormat> {
let mut map = HashMap::new();
for link in &dataview.relation_links {
if let Ok(format) = model::RelationFormat::try_from(link.format) {
map.insert(link.key.clone(), format);
}
}
map
}
fn relation_name_index(details: &[model::object_view::DetailsSet]) -> HashMap<String, String> {
let mut map = HashMap::new();
for detail_set in details {
let Some(details) = detail_set.details.as_ref() else {
continue;
};
let relation_key =
string_field(details, "relationKey").or_else(|| string_field(details, "key"));
let name = string_field(details, "name");
if let (Some(relation_key), Some(name)) = (relation_key, name) {
map.insert(relation_key, name);
}
}
map
}
fn string_field(details: &prost_types::Struct, key: &str) -> Option<String> {
details.fields.get(key).and_then(|value| match &value.kind {
Some(Kind::StringValue(value)) => Some(value.clone()),
_ => None,
})
}
#[cfg(test)]
mod tests {
use crate::error::AuthError;
use super::{ViewError, authenticated_request};
#[test]
fn invalid_view_token_is_preserved_as_typed_auth_error() {
let error = authenticated_request((), "SECRET_VIEW_TOKEN\n")
.expect_err("a newline is invalid in an ASCII metadata value");
assert!(matches!(
error,
ViewError::Auth {
source: AuthError::InvalidMetadata { .. }
}
));
assert!(!error.to_string().contains("SECRET_VIEW_TOKEN"));
}
}