// B2b — Tauri commands wrapping kon_storage templates CRUD. // // The frontend stores templates as `{ name, sections }` objects. This // command layer is responsible for the JSON (de)serialisation of the // `sections` column (the storage layer keeps it as opaque TEXT) and for // minting fresh ids during the localStorage → SQLite import. use serde::{Deserialize, Serialize}; use kon_storage::{ create_template as db_create_template, delete_template as db_delete_template, import_templates as db_import_templates, list_templates as db_list_templates, update_template as db_update_template, ImportSummary, TemplateRow, }; use crate::AppState; /// Frontend-facing template shape. Sections come over the wire as a real /// array — the JSON encoding is an implementation detail of the storage /// row that callers shouldn't have to think about. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TemplateDto { pub id: String, pub name: String, pub sections: Vec, pub created_at: String, pub updated_at: String, } impl TryFrom for TemplateDto { type Error = String; fn try_from(r: TemplateRow) -> Result { // Malformed sections_json is a soft failure: log + return empty // sections rather than failing the whole list. A user will see // an empty template they can edit, instead of a broken page. let sections: Vec = match serde_json::from_str(&r.sections_json) { Ok(v) => v, Err(e) => { eprintln!( "[templates] sections_json parse failed for template {} ({}): {}", r.id, r.name, e ); Vec::new() } }; Ok(Self { id: r.id, name: r.name, sections, created_at: r.created_at, updated_at: r.updated_at, }) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateTemplateRequest { pub id: String, pub name: String, pub sections: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateTemplatePatch { #[serde(default)] pub name: Option, #[serde(default)] pub sections: Option>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TemplateImport { /// Optional — if absent, the server mints a uuid v4. The localStorage /// shape pre-B2b had no id, so the import path needs to backfill it. #[serde(default)] pub id: Option, pub name: String, pub sections: Vec, /// Accepted but ignored — the DB sets datetime('now') at insert time. #[serde(default)] pub created_at: Option, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ImportSummaryDto { pub imported: u32, pub skipped: u32, } impl From for ImportSummaryDto { fn from(s: ImportSummary) -> Self { Self { imported: s.imported as u32, skipped: s.skipped as u32, } } } #[tauri::command] pub async fn list_templates_cmd( state: tauri::State<'_, AppState>, ) -> Result, String> { let rows = db_list_templates(&state.db).await.map_err(|e| e.to_string())?; let mut out = Vec::with_capacity(rows.len()); for r in rows { // TryFrom returns Ok with an empty sections vec on parse failure — // there's no error path that bubbles up here today, but keep the // ?-style plumbing in case the contract tightens. out.push(TemplateDto::try_from(r)?); } Ok(out) } #[tauri::command] pub async fn create_template_cmd( state: tauri::State<'_, AppState>, request: CreateTemplateRequest, ) -> Result { let sections_json = serde_json::to_string(&request.sections) .map_err(|e| format!("create_template: serialise sections failed: {e}"))?; let row = db_create_template(&state.db, &request.id, &request.name, §ions_json) .await .map_err(|e| e.to_string())?; TemplateDto::try_from(row) } #[tauri::command] pub async fn update_template_cmd( state: tauri::State<'_, AppState>, id: String, patch: UpdateTemplatePatch, ) -> Result { let sections_json = match patch.sections { Some(s) => Some( serde_json::to_string(&s) .map_err(|e| format!("update_template: serialise sections failed: {e}"))?, ), None => None, }; let row = db_update_template( &state.db, &id, patch.name.as_deref(), sections_json.as_deref(), ) .await .map_err(|e| e.to_string())?; TemplateDto::try_from(row) } #[tauri::command] pub async fn delete_template_cmd( state: tauri::State<'_, AppState>, id: String, ) -> Result<(), String> { db_delete_template(&state.db, &id) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn import_templates_cmd( state: tauri::State<'_, AppState>, items: Vec, ) -> Result { let mut rows: Vec = Vec::with_capacity(items.len()); for item in items { let sections_json = serde_json::to_string(&item.sections) .map_err(|e| format!("import_templates: serialise sections failed: {e}"))?; let id = item.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); rows.push(TemplateRow { id, name: item.name, sections_json, // The DB column has DEFAULT datetime('now'); these placeholder // strings are overwritten by the INSERT for the live row and // are only used to build the in-memory TemplateRow shape // passed to import_templates. created_at: item.created_at.unwrap_or_default(), updated_at: String::new(), }); } db_import_templates(&state.db, &rows) .await .map(ImportSummaryDto::from) .map_err(|e| e.to_string()) }