BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1use super::error::{AppError, Result};
2use super::state::AppState;
3use rusqlite::params;
4use serde::Serialize;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct Column {
10 pub id: String,
11 pub account_did: String,
12 pub kind: String,
13 pub config: String,
14 pub position: i64,
15 pub width: String,
16 pub created_at: String,
17}
18
19pub fn get_columns(account_did: &str, state: &AppState) -> Result<Vec<Column>> {
20 let conn = state.auth_store.lock_connection()?;
21 let mut stmt = conn.prepare(
22 "SELECT id, account_did, kind, config, position, width, created_at
23 FROM columns
24 WHERE account_did = ?1
25 ORDER BY position ASC",
26 )?;
27
28 let rows = stmt.query_map(params![account_did], |row| {
29 Ok(Column {
30 id: row.get(0)?,
31 account_did: row.get(1)?,
32 kind: row.get(2)?,
33 config: row.get(3)?,
34 position: row.get(4)?,
35 width: row.get(5)?,
36 created_at: row.get(6)?,
37 })
38 })?;
39
40 let mut columns = Vec::new();
41 for row in rows {
42 columns.push(row?);
43 }
44 Ok(columns)
45}
46
47pub fn add_column(
48 account_did: &str, kind: &str, config: &str, position: Option<u32>, state: &AppState,
49) -> Result<Column> {
50 validate_kind(kind)?;
51 validate_config_json(config)?;
52
53 let conn = state.auth_store.lock_connection()?;
54
55 let insert_position = match position {
56 Some(pos) => {
57 conn.execute(
58 "UPDATE columns SET position = position + 1
59 WHERE account_did = ?1 AND position >= ?2",
60 params![account_did, pos],
61 )?;
62 pos as i64
63 }
64 None => {
65 let max: Option<i64> = conn
66 .query_row(
67 "SELECT MAX(position) FROM columns WHERE account_did = ?1",
68 params![account_did],
69 |row| row.get(0),
70 )
71 .unwrap_or(None);
72 max.map(|m| m + 1).unwrap_or(0)
73 }
74 };
75
76 let id = Uuid::new_v4().to_string();
77 conn.execute(
78 "INSERT INTO columns(id, account_did, kind, config, position, width)
79 VALUES (?1, ?2, ?3, ?4, ?5, 'standard')",
80 params![id, account_did, kind, config, insert_position],
81 )?;
82
83 let column = conn.query_row(
84 "SELECT id, account_did, kind, config, position, width, created_at
85 FROM columns WHERE id = ?1",
86 params![id],
87 |row| {
88 Ok(Column {
89 id: row.get(0)?,
90 account_did: row.get(1)?,
91 kind: row.get(2)?,
92 config: row.get(3)?,
93 position: row.get(4)?,
94 width: row.get(5)?,
95 created_at: row.get(6)?,
96 })
97 },
98 )?;
99
100 Ok(column)
101}
102
103pub fn remove_column(id: &str, state: &AppState) -> Result<()> {
104 let conn = state.auth_store.lock_connection()?;
105
106 let affected = conn.execute("DELETE FROM columns WHERE id = ?1", params![id])?;
107
108 if affected == 0 {
109 return Err(AppError::validation(format!("column '{id}' not found")));
110 }
111
112 Ok(())
113}
114
115pub fn reorder_columns(ids: &[String], state: &AppState) -> Result<()> {
116 if ids.is_empty() {
117 return Ok(());
118 }
119
120 let conn = state.auth_store.lock_connection()?;
121
122 for (position, id) in ids.iter().enumerate() {
123 conn.execute(
124 "UPDATE columns SET position = ?1 WHERE id = ?2",
125 params![position as i64, id],
126 )?;
127 }
128
129 Ok(())
130}
131
132pub fn update_column(id: &str, config: Option<&str>, width: Option<&str>, state: &AppState) -> Result<Column> {
133 if config.is_none() && width.is_none() {
134 return Err(AppError::validation("at least one of config or width must be provided"));
135 }
136
137 if let Some(c) = config {
138 validate_config_json(c)?;
139 }
140
141 if let Some(w) = width {
142 validate_width(w)?;
143 }
144
145 let conn = state.auth_store.lock_connection()?;
146
147 let exists: bool = conn
148 .query_row("SELECT 1 FROM columns WHERE id = ?1", params![id], |_| Ok(true))
149 .unwrap_or(false);
150
151 if !exists {
152 return Err(AppError::validation(format!("column '{id}' not found")));
153 }
154
155 if let Some(c) = config {
156 conn.execute("UPDATE columns SET config = ?1 WHERE id = ?2", params![c, id])?;
157 }
158
159 if let Some(w) = width {
160 conn.execute("UPDATE columns SET width = ?1 WHERE id = ?2", params![w, id])?;
161 }
162
163 let column = conn.query_row(
164 "SELECT id, account_did, kind, config, position, width, created_at
165 FROM columns WHERE id = ?1",
166 params![id],
167 |row| {
168 Ok(Column {
169 id: row.get(0)?,
170 account_did: row.get(1)?,
171 kind: row.get(2)?,
172 config: row.get(3)?,
173 position: row.get(4)?,
174 width: row.get(5)?,
175 created_at: row.get(6)?,
176 })
177 },
178 )?;
179
180 Ok(column)
181}
182
183fn validate_kind(kind: &str) -> Result<()> {
184 match kind {
185 "feed" | "explorer" | "diagnostics" | "messages" | "search" | "profile" => Ok(()),
186 _ => Err(AppError::validation(format!(
187 "invalid column kind '{kind}': must be 'feed', 'explorer', 'diagnostics', 'messages', 'search', or 'profile'"
188 ))),
189 }
190}
191
192fn validate_width(width: &str) -> Result<()> {
193 match width {
194 "narrow" | "standard" | "wide" => Ok(()),
195 _ => Err(AppError::validation(format!(
196 "invalid column width '{width}': must be 'narrow', 'standard', or 'wide'"
197 ))),
198 }
199}
200
201fn validate_config_json(config: &str) -> Result<()> {
202 serde_json::from_str::<serde_json::Value>(config)
203 .map(|_| ())
204 .map_err(|e| AppError::validation(format!("config must be valid JSON: {e}")))
205}