Skip to main content

anytype_rpc/
views.rs

1//! Helpers for working with dataview-based views.
2
3use 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/// Column metadata for a grid (table) view.
21#[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/// Grid (table) view metadata and columns.
32#[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
40/// Fetch table/list view column metadata for a type object.
41pub 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}