1use anyhow::Result;
6use serde::{Deserialize, Serialize};
7
8const TABLE: &str = "settings";
9
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
12pub struct TeamSettings {
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub name: Option<String>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub llm_base_url: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub llm_model: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub embed_model: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub enrich: Option<bool>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub flows: Option<bool>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub guide: Option<bool>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub embed: Option<bool>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub max_enrich: Option<usize>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub max_flows: Option<usize>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub llm_concurrency: Option<usize>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub llm_price_in: Option<f64>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub llm_price_out: Option<f64>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub format: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub scip: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub auto_index: Option<bool>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub auto_index_llm: Option<bool>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub auto_index_minutes: Option<usize>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub repos: Vec<String>,
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct Stored {
68 pub settings: TeamSettings,
69 pub version: Option<u64>,
70 pub changed_by: Option<String>,
71 pub changed_at: Option<String>,
72}
73
74fn uri(root: &str) -> String {
75 format!("{}/_team", root.trim_end_matches('/'))
76}
77
78pub async fn load(root: &str) -> Result<Stored> {
80 let s = crate::docstore::load::<TeamSettings>(&uri(root), TABLE).await?;
81 Ok(Stored {
82 settings: s.doc,
83 version: s.version,
84 changed_by: s.changed_by,
85 changed_at: s.changed_at,
86 })
87}
88
89pub async fn save_as(
91 root: &str,
92 settings: &TeamSettings,
93 expected: Option<u64>,
94 by: &str,
95) -> Result<u64> {
96 crate::docstore::save(&uri(root), TABLE, settings, expected, by)
97 .await
98 .map_err(|e| match format!("{e}").contains("changed meanwhile") {
99 true => anyhow::anyhow!(
100 "the team settings were changed by someone else; reload and try again"
101 ),
102 false => e,
103 })
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[tokio::test]
111 async fn save_load_and_refuse_a_stale_write() {
112 let dir = std::env::temp_dir().join(format!("comindly-team-{}", std::process::id()));
113 let root = dir.to_str().unwrap();
114 let first = TeamSettings {
115 llm_model: Some("m1".into()),
116 ..Default::default()
117 };
118 let v1 = save_as(root, &first, None, "t").await.unwrap();
119 assert_eq!(load(root).await.unwrap().settings, first);
120 let second = TeamSettings {
121 llm_model: Some("m2".into()),
122 ..Default::default()
123 };
124 save_as(root, &second, Some(v1), "t").await.unwrap();
125 assert!(
126 save_as(root, &first, Some(v1), "t").await.is_err(),
127 "stale version"
128 );
129 assert_eq!(
130 load(root).await.unwrap().settings.llm_model.as_deref(),
131 Some("m2")
132 );
133 std::fs::remove_dir_all(&dir).ok();
134 }
135}