//! Project storage. use std::sync::Arc; use iam_types::{Error, IamError, Project, Result}; use crate::backend::{Backend, CasResult, JsonStore, StorageBackend}; mod keys { pub const PROJECTS: &str = "iam/projects/"; pub const BY_ORG: &str = "iam/projects/by-org/"; } pub struct ProjectStore { backend: Arc, } impl JsonStore for ProjectStore { fn backend(&self) -> &Backend { &self.backend } } impl ProjectStore { pub fn new(backend: Arc) -> Self { Self { backend } } pub async fn create(&self, project: &Project) -> Result { let key = self.primary_key(&project.org_id, &project.id); let bytes = serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?; match self.backend.cas(key.as_bytes(), 0, &bytes).await? { CasResult::Success(version) => { self.create_indexes(project).await?; Ok(version) } CasResult::Conflict { .. } => { Err(Error::Iam(IamError::ProjectAlreadyExists(project.key()))) } CasResult::NotFound => Err(Error::Internal("Unexpected CAS result".into())), } } pub async fn create_if_missing(&self, project: &Project) -> Result { let key = self.primary_key(&project.org_id, &project.id); let bytes = serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?; match self.backend.cas(key.as_bytes(), 0, &bytes).await? { CasResult::Success(_) => { self.create_indexes(project).await?; Ok(true) } CasResult::Conflict { .. } => Ok(false), CasResult::NotFound => Err(Error::Internal("Unexpected CAS result".into())), } } pub async fn get(&self, org_id: &str, id: &str) -> Result> { Ok(self .get_json::(self.primary_key(org_id, id).as_bytes()) .await? .map(|v| v.0)) } pub async fn get_with_version(&self, org_id: &str, id: &str) -> Result> { self.get_json::(self.primary_key(org_id, id).as_bytes()) .await } pub async fn update(&self, project: &Project, expected_version: u64) -> Result { let key = self.primary_key(&project.org_id, &project.id); let bytes = serde_json::to_vec(project).map_err(|e| Error::Serialization(e.to_string()))?; match self .backend .cas(key.as_bytes(), expected_version, &bytes) .await? { CasResult::Success(version) => { self.create_indexes(project).await?; Ok(version) } CasResult::Conflict { expected, actual } => { Err(Error::Storage(iam_types::StorageError::CasConflict { expected, actual, })) } CasResult::NotFound => Err(Error::Iam(IamError::ProjectNotFound(project.key()))), } } pub async fn delete(&self, org_id: &str, id: &str) -> Result { if let Some(project) = self.get(org_id, id).await? { let deleted = self .backend .delete(self.primary_key(org_id, id).as_bytes()) .await?; if deleted { self.delete_indexes(&project).await?; } Ok(deleted) } else { Ok(false) } } pub async fn list(&self) -> Result> { let pairs = self .backend .scan_prefix(keys::PROJECTS.as_bytes(), 10_000) .await?; let mut projects = Vec::new(); for pair in pairs { if String::from_utf8_lossy(&pair.key).starts_with(keys::BY_ORG) { continue; } let project: Project = serde_json::from_slice(&pair.value) .map_err(|e| Error::Serialization(e.to_string()))?; projects.push(project); } Ok(projects) } pub async fn list_by_org(&self, org_id: &str) -> Result> { let prefix = format!("{}{}/", keys::BY_ORG, org_id); let pairs = self.backend.scan_prefix(prefix.as_bytes(), 10_000).await?; let mut projects = Vec::new(); for pair in pairs { let project_id = String::from_utf8_lossy(&pair.value).to_string(); if let Some(project) = self.get(org_id, &project_id).await? { projects.push(project); } } Ok(projects) } pub async fn exists(&self, org_id: &str, id: &str) -> Result { Ok(self .backend .get(self.primary_key(org_id, id).as_bytes()) .await? .is_some()) } fn primary_key(&self, org_id: &str, id: &str) -> String { format!("{}{}/{}", keys::PROJECTS, org_id, id) } async fn create_indexes(&self, project: &Project) -> Result<()> { let key = format!("{}{}/{}", keys::BY_ORG, project.org_id, project.id); self.backend .put(key.as_bytes(), project.id.as_bytes()) .await?; Ok(()) } async fn delete_indexes(&self, project: &Project) -> Result<()> { let key = format!("{}{}/{}", keys::BY_ORG, project.org_id, project.id); self.backend.delete(key.as_bytes()).await?; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn list_by_org_is_isolated() { let backend = Arc::new(Backend::memory()); let store = ProjectStore::new(backend); store .create(&Project::new("proj-1", "org-1", "Project 1")) .await .unwrap(); store .create(&Project::new("proj-1", "org-2", "Project 1")) .await .unwrap(); let org1 = store.list_by_org("org-1").await.unwrap(); let org2 = store.list_by_org("org-2").await.unwrap(); assert_eq!(org1.len(), 1); assert_eq!(org2.len(), 1); assert_eq!(org1[0].org_id, "org-1"); assert_eq!(org2[0].org_id, "org-2"); } }