186 lines
6.1 KiB
Rust
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");
|
|
}
|
|
}
|