photoncloud-monorepo/iam/crates/iam-store/src/project_store.rs

186 lines
6.1 KiB
Rust

//! 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<Backend>,
}
impl JsonStore for ProjectStore {
fn backend(&self) -> &Backend {
&self.backend
}
}
impl ProjectStore {
pub fn new(backend: Arc<Backend>) -> Self {
Self { backend }
}
pub async fn create(&self, project: &Project) -> Result<u64> {
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<bool> {
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<Option<Project>> {
Ok(self
.get_json::<Project>(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<Option<(Project, u64)>> {
self.get_json::<Project>(self.primary_key(org_id, id).as_bytes())
.await
}
pub async fn update(&self, project: &Project, expected_version: u64) -> Result<u64> {
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<bool> {
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<Vec<Project>> {
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<Vec<Project>> {
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<bool> {
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");
}
}