1use rusqlite::{params, types::Type};
2
3use crate::db::{SqliteStore, now_utc_ms};
4use crate::error::{CoreError, Result};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct PeerRecord {
8 pub alias: String,
9 pub did: String,
10 pub proxy_url: String,
11 pub agent_name: Option<String>,
12 pub human_name: Option<String>,
13 pub created_at_ms: i64,
14 pub updated_at_ms: i64,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct UpsertPeerInput {
19 pub alias: String,
20 pub did: String,
21 pub proxy_url: String,
22 pub agent_name: Option<String>,
23 pub human_name: Option<String>,
24}
25
26fn invalid_data_error(message: impl Into<String>) -> CoreError {
27 CoreError::InvalidInput(message.into())
28}
29
30fn parse_optional_non_empty(value: Option<String>) -> Option<String> {
31 value.and_then(|raw| {
32 let trimmed = raw.trim();
33 if trimmed.is_empty() {
34 None
35 } else {
36 Some(trimmed.to_string())
37 }
38 })
39}
40
41fn map_peer_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<PeerRecord> {
42 Ok(PeerRecord {
43 alias: row.get(0)?,
44 did: row.get(1)?,
45 proxy_url: row.get(2)?,
46 agent_name: row.get(3)?,
47 human_name: row.get(4)?,
48 created_at_ms: row.get(5)?,
49 updated_at_ms: row.get(6)?,
50 })
51}
52
53pub fn upsert_peer(store: &SqliteStore, input: UpsertPeerInput) -> Result<PeerRecord> {
55 let alias = input.alias.trim().to_string();
56 let did = input.did.trim().to_string();
57 let proxy_url = input.proxy_url.trim().to_string();
58 if alias.is_empty() {
59 return Err(invalid_data_error("peer alias is required"));
60 }
61 if did.is_empty() {
62 return Err(invalid_data_error("peer did is required"));
63 }
64 if proxy_url.is_empty() {
65 return Err(invalid_data_error("peer proxyUrl is required"));
66 }
67
68 let agent_name = parse_optional_non_empty(input.agent_name);
69 let human_name = parse_optional_non_empty(input.human_name);
70 let now_ms = now_utc_ms();
71
72 store.with_connection(|connection| {
73 connection.execute(
74 "INSERT INTO peers (
75 alias, did, proxy_url, agent_name, human_name, created_at_ms, updated_at_ms
76 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
77 ON CONFLICT(alias) DO UPDATE SET
78 did = excluded.did,
79 proxy_url = excluded.proxy_url,
80 agent_name = excluded.agent_name,
81 human_name = excluded.human_name,
82 updated_at_ms = excluded.updated_at_ms",
83 params![&alias, &did, &proxy_url, &agent_name, &human_name, now_ms],
84 )?;
85
86 get_peer(connection, &alias).ok_or_else(|| {
87 CoreError::Sqlite(rusqlite::Error::FromSqlConversionFailure(
88 0,
89 Type::Text,
90 "upsert_peer lost row".into(),
91 ))
92 })
93 })
94}
95
96pub fn get_peer_by_alias(store: &SqliteStore, alias: &str) -> Result<Option<PeerRecord>> {
98 let alias = alias.trim().to_string();
99 if alias.is_empty() {
100 return Ok(None);
101 }
102 store.with_connection(|connection| Ok(get_peer(connection, &alias)))
103}
104
105fn get_peer(connection: &rusqlite::Connection, alias: &str) -> Option<PeerRecord> {
106 let mut statement = connection
107 .prepare(
108 "SELECT alias, did, proxy_url, agent_name, human_name, created_at_ms, updated_at_ms
109 FROM peers WHERE alias = ?1",
110 )
111 .ok()?;
112 statement.query_row([alias], map_peer_row).ok()
113}
114
115pub fn list_peers(store: &SqliteStore) -> Result<Vec<PeerRecord>> {
117 store.with_connection(|connection| {
118 let mut statement = connection.prepare(
119 "SELECT alias, did, proxy_url, agent_name, human_name, created_at_ms, updated_at_ms
120 FROM peers
121 ORDER BY alias ASC",
122 )?;
123 let rows = statement.query_map([], map_peer_row)?;
124 let peers: rusqlite::Result<Vec<PeerRecord>> = rows.collect();
125 Ok(peers?)
126 })
127}
128
129pub fn delete_peer(store: &SqliteStore, alias: &str) -> Result<bool> {
131 let alias = alias.trim().to_string();
132 if alias.is_empty() {
133 return Ok(false);
134 }
135 store.with_connection(|connection| {
136 let deleted = connection.execute("DELETE FROM peers WHERE alias = ?1", [alias])?;
137 Ok(deleted > 0)
138 })
139}
140
141#[cfg(test)]
142mod tests {
143 use tempfile::TempDir;
144
145 use crate::db::SqliteStore;
146
147 use super::{UpsertPeerInput, delete_peer, get_peer_by_alias, list_peers, upsert_peer};
148
149 #[test]
150 fn upsert_list_get_delete_peer_records() {
151 let temp = TempDir::new().expect("temp dir");
152 let store = SqliteStore::open_path(temp.path().join("db.sqlite3")).expect("open db");
153
154 let inserted = upsert_peer(
155 &store,
156 UpsertPeerInput {
157 alias: "alpha".to_string(),
158 did: "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"
159 .to_string(),
160 proxy_url: "https://proxy.example".to_string(),
161 agent_name: Some("Alpha".to_string()),
162 human_name: Some("Alice".to_string()),
163 },
164 )
165 .expect("insert peer");
166 assert_eq!(inserted.alias, "alpha");
167
168 let fetched = get_peer_by_alias(&store, "alpha")
169 .expect("get peer")
170 .expect("peer");
171 assert_eq!(
172 fetched.did,
173 "did:cdi:registry.clawdentity.com:agent:01HF7YAT00W6W7CM7N3W5FDXT4"
174 );
175
176 let listed = list_peers(&store).expect("list peers");
177 assert_eq!(listed.len(), 1);
178
179 let deleted = delete_peer(&store, "alpha").expect("delete peer");
180 assert!(deleted);
181 assert!(
182 get_peer_by_alias(&store, "alpha")
183 .expect("get deleted")
184 .is_none()
185 );
186 }
187}