use std::sync::Arc;
use crate::errors::Result;
use async_trait::async_trait;
use atproto_identity::model::Document;
use atproto_identity::storage::DidDocumentStorage;
use chrono::Utc;
use sqlx::sqlite::SqlitePool;
use super::{Award, AwardWithBadge, Badge, Identity, Storage};
/// SQLite storage implementation for badges and awards.
#[derive(Debug, Clone)]
pub struct SqliteStorage {
pool: SqlitePool,
}
impl SqliteStorage {
/// Create a new SQLite storage instance with the given connection pool.
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
async fn migrate(&self) -> Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS identities (
did TEXT PRIMARY KEY,
handle TEXT NOT NULL,
record JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_identities_handle ON identities(handle);
CREATE INDEX IF NOT EXISTS idx_identities_created_at ON identities(created_at);
CREATE INDEX IF NOT EXISTS idx_identities_updated_at ON identities(updated_at);
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS awards (
aturi TEXT PRIMARY KEY,
cid TEXT NOT NULL,
did TEXT NOT NULL,
badge TEXT NOT NULL,
badge_cid TEXT NOT NULL,
badge_name TEXT NOT NULL,
validated_issuers JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
record JSON NOT NULL
);
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_awards_did ON awards(did);
CREATE INDEX IF NOT EXISTS idx_awards_badge ON awards(badge);
CREATE INDEX IF NOT EXISTS idx_awards_badge_cid ON awards(badge_cid);
CREATE INDEX IF NOT EXISTS idx_awards_created_at ON awards(created_at);
CREATE INDEX IF NOT EXISTS idx_awards_updated_at ON awards(updated_at);
"#,
)
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS badges (
aturi TEXT NOT NULL,
cid TEXT NOT NULL,
name TEXT NOT NULL,
image TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
count INTEGER NOT NULL DEFAULT 0,
record JSON NOT NULL DEFAULT '{}',
PRIMARY KEY (aturi, cid)
);
"#,
)
.execute(&self.pool)
.await?;
// Add record column to existing badges table if it doesn't exist
sqlx::query(
r#"
ALTER TABLE badges ADD COLUMN record JSON NOT NULL DEFAULT '{}';
"#,
)
.execute(&self.pool)
.await
.ok(); // Ignore error if column already exists
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_badges_aturi ON badges(aturi);
CREATE INDEX IF NOT EXISTS idx_badges_cid ON badges(cid);
CREATE INDEX IF NOT EXISTS idx_badges_created_at ON badges(created_at);
CREATE INDEX IF NOT EXISTS idx_badges_updated_at ON badges(updated_at);
CREATE INDEX IF NOT EXISTS idx_badges_record ON badges(json_extract(record, '$.name'));
"#,
)
.execute(&self.pool)
.await?;
Ok(())
}
async fn upsert_identity(&self, identity: &Identity) -> Result<()> {
sqlx::query(
r#"
INSERT INTO identities (did, handle, record, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT(did) DO UPDATE SET
handle = EXCLUDED.handle,
record = EXCLUDED.record,
updated_at = EXCLUDED.updated_at
"#,
)
.bind(&identity.did)
.bind(&identity.handle)
.bind(&identity.record)
.bind(identity.created_at)
.bind(identity.updated_at)
.execute(&self.pool)
.await?;
Ok(())
}
async fn get_identity_by_did(&self, did: &str) -> Result