A Rust application to showcase badge awards in the AT Protocol ecosystem.
1//! Database storage implementations for badges, awards, and identities.
2//!
3//! Provides trait-based storage abstraction with SQLite and PostgreSQL backends,
4//! plus file storage for badge images with local and S3 support.
5
6use crate::errors::Result;
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12// Re-export storage implementations
13/// File storage implementations for badge images.
14pub mod file_storage;
15#[cfg(feature = "postgres")]
16/// PostgreSQL storage implementation.
17pub mod postgres;
18#[cfg(feature = "sqlite")]
19/// SQLite storage implementation.
20pub mod sqlite;
21
22#[cfg(feature = "s3")]
23pub use file_storage::S3FileStorage;
24pub use file_storage::{FileStorage, LocalFileStorage};
25#[cfg(feature = "postgres")]
26pub use postgres::*;
27#[cfg(feature = "sqlite")]
28pub use sqlite::*;
29
30/// Identity information for a DID.
31#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
32pub struct Identity {
33 /// Decentralized identifier.
34 pub did: String,
35 /// AT Protocol handle.
36 pub handle: String,
37 /// Full DID document JSON.
38 pub record: Value,
39 /// Record creation timestamp.
40 pub created_at: DateTime<Utc>,
41 /// Record last update timestamp.
42 pub updated_at: DateTime<Utc>,
43}
44
45/// Badge award record.
46#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
47pub struct Award {
48 /// AT-URI of the award record.
49 pub aturi: String,
50 /// Content identifier for the award record.
51 pub cid: String,
52 /// DID of the award recipient.
53 pub did: String,
54 /// AT-URI of the associated badge.
55 pub badge: String,
56 /// Content identifier of the badge record.
57 pub badge_cid: String,
58 /// Human-readable badge name.
59 pub badge_name: String,
60 /// JSON array of validated issuer DIDs.
61 pub validated_issuers: Value,
62 /// Record creation timestamp.
63 pub created_at: DateTime<Utc>,
64 /// Record last update timestamp.
65 pub updated_at: DateTime<Utc>,
66 /// Full JSON record of the award.
67 pub record: Value,
68}
69
70/// Badge definition record.
71#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
72pub struct Badge {
73 /// AT-URI of the badge definition.
74 pub aturi: String,
75 /// Content identifier for the badge record.
76 pub cid: String,
77 /// Human-readable badge name.
78 pub name: String,
79 /// Optional image reference (blob CID).
80 pub image: Option<String>,
81 /// Record creation timestamp.
82 pub created_at: DateTime<Utc>,
83 /// Record last update timestamp.
84 pub updated_at: DateTime<Utc>,
85 /// Number of awards using this badge.
86 pub count: i64,
87 /// Full JSON record of the badge definition.
88 pub record: Value,
89}
90
91/// Award with enriched badge and identity information.
92#[derive(Debug, Clone)]
93pub struct AwardWithBadge {
94 /// The award record.
95 pub award: Award,
96 /// Associated badge information if available.
97 pub badge: Option<Badge>,
98 /// Recipient identity information if available.
99 pub identity: Option<Identity>,
100 /// Identity information for award signers.
101 pub signer_identities: Vec<Identity>,
102}
103
104/// Storage trait defining the interface for badge storage operations
105#[async_trait]
106pub trait Storage: Send + Sync {
107 /// Run database migrations
108 async fn migrate(&self) -> Result<()>;
109
110 /// Insert or update an identity
111 async fn upsert_identity(&self, identity: &Identity) -> Result<()>;
112
113 /// Get an identity by DID
114 async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>>;
115
116 /// Get an identity by handle
117 async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>>;
118
119 /// Insert or update a badge
120 async fn upsert_badge(&self, badge: &Badge) -> Result<()>;
121
122 /// Get a badge by AT-URI and CID
123 async fn get_badge(&self, aturi: &str, cid: &str) -> Result<Option<Badge>>;
124
125 /// Increment the count for a badge
126 async fn increment_badge_count(&self, aturi: &str, cid: &str) -> Result<()>;
127
128 /// Decrement the count for a badge
129 async fn decrement_badge_count(&self, aturi: &str, cid: &str) -> Result<()>;
130
131 /// Insert or update an award, returns true if it's a new award
132 async fn upsert_award(&self, award: &Award) -> Result<bool>;
133
134 /// Get an award by AT-URI
135 async fn get_award(&self, aturi: &str) -> Result<Option<Award>>;
136
137 /// Delete an award by AT-URI
138 async fn delete_award(&self, aturi: &str) -> Result<Option<Award>>;
139
140 /// Trim awards for a DID to keep only the most recent ones
141 async fn trim_awards_for_did(&self, did: &str, max_count: i64) -> Result<()>;
142
143 /// Get recent awards with enriched badge and identity information
144 async fn get_recent_awards(&self, limit: i64) -> Result<Vec<AwardWithBadge>>;
145
146 /// Get awards for a specific DID with enriched information
147 async fn get_awards_for_did(&self, did: &str, limit: i64) -> Result<Vec<AwardWithBadge>>;
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use chrono::Utc;
154 use serde_json::json;
155
156 #[test]
157 fn test_badge_with_record() {
158 let record = json!({
159 "name": "Test Badge",
160 "description": "A test badge for unit testing",
161 "image": {
162 "$type": "blob",
163 "ref": {"$link": "bafkreiabc123"},
164 "mimeType": "image/png",
165 "size": 1024
166 }
167 });
168
169 let badge = Badge {
170 aturi: "at://did:plc:test/community.lexicon.badge.definition/test".to_string(),
171 cid: "bafyreiabc123".to_string(),
172 name: "Test Badge".to_string(),
173 image: Some("bafkreiabc123".to_string()),
174 created_at: Utc::now(),
175 updated_at: Utc::now(),
176 count: 0,
177 record: record.clone(),
178 };
179
180 // Verify the badge contains the record
181 assert_eq!(badge.record, record);
182 assert_eq!(badge.name, "Test Badge");
183 assert_eq!(badge.record["name"], "Test Badge");
184 assert_eq!(badge.record["description"], "A test badge for unit testing");
185 }
186
187 #[test]
188 fn test_badge_record_serialization() {
189 let record = json!({
190 "name": "Serialization Test",
191 "description": "Testing JSON serialization",
192 "issuer": "did:plc:issuer"
193 });
194
195 let badge = Badge {
196 aturi: "at://did:plc:test/community.lexicon.badge.definition/serial".to_string(),
197 cid: "bafyreiserial".to_string(),
198 name: "Serialization Test".to_string(),
199 image: None,
200 created_at: Utc::now(),
201 updated_at: Utc::now(),
202 count: 5,
203 record: record.clone(),
204 };
205
206 // Test that we can serialize and deserialize the badge
207 let serialized = serde_json::to_string(&badge).expect("Failed to serialize badge");
208 let deserialized: Badge =
209 serde_json::from_str(&serialized).expect("Failed to deserialize badge");
210
211 assert_eq!(deserialized.record, record);
212 assert_eq!(deserialized.name, badge.name);
213 assert_eq!(deserialized.count, badge.count);
214 }
215}