clawdentity_core/db/
verify_cache.rs1use rusqlite::{OptionalExtension, params};
2
3use crate::db::{SqliteStore, now_utc_ms};
4use crate::error::{CoreError, Result};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct VerifyCacheEntry {
8 pub cache_key: String,
9 pub registry_url: String,
10 pub fetched_at_ms: i64,
11 pub payload_json: String,
12}
13
14pub fn upsert_verify_cache_entry(
16 store: &SqliteStore,
17 cache_key: &str,
18 registry_url: &str,
19 payload_json: &str,
20) -> Result<()> {
21 let cache_key = cache_key.trim();
22 let registry_url = registry_url.trim();
23 let payload_json = payload_json.trim();
24 if cache_key.is_empty() {
25 return Err(CoreError::InvalidInput("cache_key is required".to_string()));
26 }
27 if registry_url.is_empty() {
28 return Err(CoreError::InvalidInput(
29 "registry_url is required".to_string(),
30 ));
31 }
32 if payload_json.is_empty() {
33 return Err(CoreError::InvalidInput(
34 "payload_json is required".to_string(),
35 ));
36 }
37
38 let fetched_at_ms = now_utc_ms();
39 store.with_connection(|connection| {
40 connection.execute(
41 "INSERT INTO verify_cache (cache_key, registry_url, fetched_at_ms, payload_json)
42 VALUES (?1, ?2, ?3, ?4)
43 ON CONFLICT(cache_key) DO UPDATE SET
44 registry_url = excluded.registry_url,
45 fetched_at_ms = excluded.fetched_at_ms,
46 payload_json = excluded.payload_json",
47 params![cache_key, registry_url, fetched_at_ms, payload_json],
48 )?;
49 Ok(())
50 })
51}
52
53pub fn get_verify_cache_entry(
55 store: &SqliteStore,
56 cache_key: &str,
57) -> Result<Option<VerifyCacheEntry>> {
58 let cache_key = cache_key.trim();
59 if cache_key.is_empty() {
60 return Ok(None);
61 }
62
63 store.with_connection(|connection| {
64 let mut statement = connection.prepare(
65 "SELECT cache_key, registry_url, fetched_at_ms, payload_json
66 FROM verify_cache
67 WHERE cache_key = ?1",
68 )?;
69 let result = statement
70 .query_row([cache_key], |row| {
71 Ok(VerifyCacheEntry {
72 cache_key: row.get(0)?,
73 registry_url: row.get(1)?,
74 fetched_at_ms: row.get(2)?,
75 payload_json: row.get(3)?,
76 })
77 })
78 .optional()?;
79 Ok(result)
80 })
81}
82
83pub fn delete_verify_cache_entry(store: &SqliteStore, cache_key: &str) -> Result<bool> {
85 let cache_key = cache_key.trim();
86 if cache_key.is_empty() {
87 return Ok(false);
88 }
89 store.with_connection(|connection| {
90 let deleted =
91 connection.execute("DELETE FROM verify_cache WHERE cache_key = ?1", [cache_key])?;
92 Ok(deleted > 0)
93 })
94}
95
96pub fn purge_verify_cache_before(store: &SqliteStore, cutoff_ms: i64) -> Result<usize> {
98 store.with_connection(|connection| {
99 let deleted = connection.execute(
100 "DELETE FROM verify_cache WHERE fetched_at_ms < ?1",
101 [cutoff_ms],
102 )?;
103 Ok(deleted)
104 })
105}
106
107#[cfg(test)]
108mod tests {
109 use tempfile::TempDir;
110
111 use crate::db::SqliteStore;
112
113 use super::{
114 delete_verify_cache_entry, get_verify_cache_entry, purge_verify_cache_before,
115 upsert_verify_cache_entry,
116 };
117
118 #[test]
119 fn upsert_get_delete_verify_cache_entry() {
120 let temp = TempDir::new().expect("temp dir");
121 let store = SqliteStore::open_path(temp.path().join("db.sqlite3")).expect("open db");
122
123 upsert_verify_cache_entry(
124 &store,
125 "keys::https://registry.clawdentity.com",
126 "https://registry.clawdentity.com",
127 "{\"keys\":[]}",
128 )
129 .expect("upsert");
130
131 let entry = get_verify_cache_entry(&store, "keys::https://registry.clawdentity.com")
132 .expect("get")
133 .expect("entry");
134 assert_eq!(entry.registry_url, "https://registry.clawdentity.com");
135 assert_eq!(entry.payload_json, "{\"keys\":[]}");
136
137 let purged = purge_verify_cache_before(&store, i64::MAX).expect("purge");
138 assert_eq!(purged, 1);
139 assert!(
140 get_verify_cache_entry(&store, "keys::https://registry.clawdentity.com")
141 .expect("get after purge")
142 .is_none()
143 );
144
145 let deleted = delete_verify_cache_entry(&store, "keys::https://registry.clawdentity.com")
146 .expect("delete");
147 assert!(!deleted);
148 }
149}