Skip to main content

clawdentity_core/db/
peers.rs

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
53/// TODO(clawdentity): document `upsert_peer`.
54pub 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
96/// TODO(clawdentity): document `get_peer_by_alias`.
97pub 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
115/// TODO(clawdentity): document `list_peers`.
116pub 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
129/// TODO(clawdentity): document `delete_peer`.
130pub 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}