reinhardt_admin/server/
export.rs1#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{AdminDatabase, AdminRecord, AdminSite, ExportFormat, ExportResponse};
8#[cfg(server)]
9use crate::core::{AdminDatabaseKey, AdminSiteKey};
10#[cfg(server)]
11use reinhardt_di::Depends;
12#[cfg(server)]
13use reinhardt_pages::server_fn::ServerFnRequest;
14use reinhardt_pages::server_fn::{ServerFnError, server_fn};
15
16#[cfg(server)]
17use super::error::{AdminAuth, MapServerFnError, ModelPermission};
18#[cfg(server)]
19use super::limits::MAX_EXPORT_RECORDS;
20
21#[cfg(server)]
27fn serialize_delimited(
28 results: &[std::collections::HashMap<String, serde_json::Value>],
29 delimiter: u8,
30) -> Result<Vec<u8>, String> {
31 use std::collections::BTreeMap;
32
33 if results.is_empty() {
34 return Ok(Vec::new());
35 }
36
37 let mut wtr = csv::WriterBuilder::new()
38 .delimiter(delimiter)
39 .from_writer(vec![]);
40
41 let flat_records: Vec<BTreeMap<String, String>> = results
43 .iter()
44 .map(|record| {
45 record
46 .iter()
47 .map(|(k, v)| {
48 let s = match v {
49 serde_json::Value::String(s) => s.clone(),
50 serde_json::Value::Null => String::new(),
51 other => other.to_string(),
52 };
53 (k.clone(), s)
54 })
55 .collect()
56 })
57 .collect();
58
59 let headers: Vec<&str> = flat_records[0].keys().map(|k| k.as_str()).collect();
61 wtr.write_record(&headers)
62 .map_err(|e| format!("header serialization failed: {}", e))?;
63
64 for record in &flat_records {
66 let values: Vec<&str> = headers
67 .iter()
68 .map(|h| record.get(*h).map(|v| v.as_str()).unwrap_or(""))
69 .collect();
70 wtr.write_record(&values)
71 .map_err(|e| format!("record serialization failed: {}", e))?;
72 }
73
74 wtr.into_inner()
75 .map_err(|e| format!("writer flush failed: {}", e))
76}
77
78#[server_fn]
103pub async fn export_data(
104 model_name: String,
105 format: crate::adapters::ExportFormat,
106 #[inject] site: Depends<AdminSiteKey, AdminSite>,
107 #[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
108 #[inject] http_request: ServerFnRequest,
109 #[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
110) -> Result<crate::adapters::ExportResponse, ServerFnError> {
111 let auth = AdminAuth::from_request(&http_request);
113 let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
114 auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::View)
115 .await?;
116 let table_name = model_admin.table_name();
117
118 let total_count = db
120 .count::<AdminRecord>(table_name, vec![])
121 .await
122 .map_server_fn_error()?;
123 let truncated = total_count > MAX_EXPORT_RECORDS;
124
125 if truncated {
126 tracing::warn!(
127 "Export for model '{}' truncated: {} total records, limit is {}",
128 model_name,
129 total_count,
130 MAX_EXPORT_RECORDS
131 );
132 }
133
134 let results = db
136 .list::<AdminRecord>(table_name, vec![], 0, MAX_EXPORT_RECORDS)
137 .await
138 .map_server_fn_error()?;
139
140 let (data, filename, content_type) = match format {
142 ExportFormat::JSON => {
143 let json = serde_json::to_vec_pretty(&results).map_err(|e| {
144 ServerFnError::serialization(format!("JSON serialization failed: {}", e))
145 })?;
146 (
147 json,
148 format!("{}.json", model_name.to_lowercase()),
149 "application/json",
150 )
151 }
152 ExportFormat::CSV => {
153 let data = serialize_delimited(&results, b',').map_err(|e| {
154 ServerFnError::serialization(format!("CSV serialization failed: {}", e))
155 })?;
156
157 (
158 data,
159 format!("{}.csv", model_name.to_lowercase()),
160 "text/csv",
161 )
162 }
163 ExportFormat::TSV => {
164 let data = serialize_delimited(&results, b'\t').map_err(|e| {
165 ServerFnError::serialization(format!("TSV serialization failed: {}", e))
166 })?;
167
168 (
169 data,
170 format!("{}.tsv", model_name.to_lowercase()),
171 "text/tab-separated-values",
172 )
173 }
174 ExportFormat::Excel => {
175 return Err(ServerFnError::application(
176 "Excel export format is not supported",
177 ));
178 }
179 ExportFormat::XML => {
180 return Err(ServerFnError::application(
181 "XML export format is not supported",
182 ));
183 }
184 };
185
186 Ok(ExportResponse {
187 data,
188 filename,
189 content_type: content_type.to_string(),
190 truncated,
191 total_count: Some(total_count),
192 })
193}