1use std::collections::HashMap;
4use std::convert::TryFrom;
5
6use prost_types::value::Kind;
7use tonic::Request;
8use tonic::body::Body;
9use tonic::codegen::{Bytes, StdError};
10
11use crate::anytype::ClientCommandsClient;
12use crate::anytype::rpc::object::show::Request as ObjectShowRequest;
13use crate::auth::with_token;
14pub use crate::error::ViewError;
15use crate::model;
16use crate::model::block::ContentValue;
17use crate::model::block::content::Dataview as BlockDataview;
18use crate::model::block::content::dataview;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct GridViewColumn {
23 pub relation_key: String,
24 pub name: String,
25 pub format: Option<model::RelationFormat>,
26 pub formula: dataview::relation::FormulaType,
27 pub is_visible: bool,
28 pub width: i32,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct GridViewInfo {
34 pub block_id: String,
35 pub view_id: String,
36 pub view_name: String,
37 pub columns: Vec<GridViewColumn>,
38}
39
40pub async fn fetch_grid_view_columns<S>(
42 client: &mut ClientCommandsClient<S>,
43 token: &str,
44 space_id: &str,
45 type_id: &str,
46 view_id: &str,
47) -> Result<GridViewInfo, ViewError>
48where
49 S: tonic::client::GrpcService<Body>,
50 S::Error: Into<StdError>,
51 S::ResponseBody: http_body::Body<Data = Bytes> + Send + 'static,
52 <S::ResponseBody as http_body::Body>::Error: Into<StdError> + Send,
53{
54 let request = ObjectShowRequest {
55 object_id: type_id.to_string(),
56 space_id: space_id.to_string(),
57 include_relations_as_dependent_objects: true,
58 ..Default::default()
59 };
60 let request = authenticated_request(request, token)?;
61
62 let response = client.object_show(request).await?.into_inner();
63 if let Some(error) = response.error
64 && error.code != 0
65 {
66 return Err(ViewError::ApiResponse {
67 code: error.code,
68 description: error.description,
69 });
70 }
71
72 let object_view = response.object_view.ok_or(ViewError::MissingObjectView)?;
73 let relation_names = relation_name_index(&object_view.details);
74
75 let (block_id, dataview) = find_dataview_block(&object_view.blocks, view_id)?;
76 let view = dataview
77 .views
78 .iter()
79 .find(|view| view.id == view_id)
80 .ok_or_else(|| ViewError::MissingView {
81 view_id: view_id.to_string(),
82 })?;
83
84 let view_type =
85 dataview::view::Type::try_from(view.r#type).unwrap_or(dataview::view::Type::Table);
86 if view_type != dataview::view::Type::Table && view_type != dataview::view::Type::List {
87 return Err(ViewError::NotSupportedView {
88 view_id: view_id.to_string(),
89 actual: view.r#type,
90 });
91 }
92
93 let relation_formats = relation_format_index(&dataview);
94 let columns = view
95 .relations
96 .iter()
97 .map(|relation| {
98 let formula = dataview::relation::FormulaType::try_from(relation.formula)
99 .unwrap_or(dataview::relation::FormulaType::None);
100 let name = relation_names
101 .get(&relation.key)
102 .cloned()
103 .unwrap_or_else(|| relation.key.clone());
104 let format = relation_formats.get(&relation.key).cloned();
105
106 GridViewColumn {
107 relation_key: relation.key.clone(),
108 name,
109 format,
110 formula,
111 is_visible: relation.is_visible,
112 width: relation.width,
113 }
114 })
115 .collect::<Vec<_>>();
116
117 Ok(GridViewInfo {
118 block_id,
119 view_id: view.id.clone(),
120 view_name: view.name.clone(),
121 columns,
122 })
123}
124
125fn authenticated_request<T>(message: T, token: &str) -> Result<Request<T>, ViewError> {
126 with_token(Request::new(message), token).map_err(ViewError::from)
127}
128
129fn find_dataview_block(
130 blocks: &[model::Block],
131 view_id: &str,
132) -> Result<(String, BlockDataview), ViewError> {
133 for block in blocks {
134 if let Some(ContentValue::Dataview(dataview)) = block.content_value.as_ref()
135 && dataview.views.iter().any(|view| view.id == view_id)
136 {
137 return Ok((block.id.clone(), dataview.clone()));
138 }
139 }
140
141 Err(ViewError::MissingDataviewBlock {
142 view_id: view_id.to_string(),
143 })
144}
145
146fn relation_format_index(dataview: &BlockDataview) -> HashMap<String, model::RelationFormat> {
147 let mut map = HashMap::new();
148 for link in &dataview.relation_links {
149 if let Ok(format) = model::RelationFormat::try_from(link.format) {
150 map.insert(link.key.clone(), format);
151 }
152 }
153 map
154}
155
156fn relation_name_index(details: &[model::object_view::DetailsSet]) -> HashMap<String, String> {
157 let mut map = HashMap::new();
158 for detail_set in details {
159 let Some(details) = detail_set.details.as_ref() else {
160 continue;
161 };
162 let relation_key =
163 string_field(details, "relationKey").or_else(|| string_field(details, "key"));
164 let name = string_field(details, "name");
165 if let (Some(relation_key), Some(name)) = (relation_key, name) {
166 map.insert(relation_key, name);
167 }
168 }
169 map
170}
171
172fn string_field(details: &prost_types::Struct, key: &str) -> Option<String> {
173 details.fields.get(key).and_then(|value| match &value.kind {
174 Some(Kind::StringValue(value)) => Some(value.clone()),
175 _ => None,
176 })
177}
178
179#[cfg(test)]
180mod tests {
181 use crate::error::AuthError;
182
183 use super::{ViewError, authenticated_request};
184
185 #[test]
186 fn invalid_view_token_is_preserved_as_typed_auth_error() {
187 let error = authenticated_request((), "SECRET_VIEW_TOKEN\n")
188 .expect_err("a newline is invalid in an ASCII metadata value");
189
190 assert!(matches!(
191 error,
192 ViewError::Auth {
193 source: AuthError::InvalidMetadata { .. }
194 }
195 ));
196 assert!(!error.to_string().contains("SECRET_VIEW_TOKEN"));
197 }
198}