2026-02-08 21:29:17 +01:00
|
|
|
use crate::event_bus::ResourceEvent;
|
2026-01-28 23:06:06 +01:00
|
|
|
use crate::{ApiError, AppState, Result};
|
|
|
|
|
use reddwarf_core::{Resource, ResourceKey};
|
2026-01-28 23:17:19 +01:00
|
|
|
use reddwarf_storage::{KVStore, KeyEncoder};
|
2026-01-28 23:06:06 +01:00
|
|
|
use reddwarf_versioning::{Change, CommitBuilder};
|
|
|
|
|
use serde::Serialize;
|
|
|
|
|
use tracing::{debug, info};
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
/// Get a resource from storage
|
2026-01-28 23:17:19 +01:00
|
|
|
pub async fn get_resource<T: Resource>(state: &AppState, key: &ResourceKey) -> Result<T> {
|
2026-01-28 23:06:06 +01:00
|
|
|
debug!("Getting resource: {}", key);
|
|
|
|
|
|
|
|
|
|
let storage_key = KeyEncoder::encode_resource_key(key);
|
|
|
|
|
let data = state
|
|
|
|
|
.storage
|
|
|
|
|
.as_ref()
|
|
|
|
|
.get(storage_key.as_bytes())?
|
|
|
|
|
.ok_or_else(|| ApiError::NotFound(format!("Resource not found: {}", key)))?;
|
|
|
|
|
|
|
|
|
|
let resource: T = serde_json::from_slice(&data)?;
|
|
|
|
|
Ok(resource)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a resource in storage
|
2026-01-28 23:17:19 +01:00
|
|
|
pub async fn create_resource<T: Resource>(state: &AppState, mut resource: T) -> Result<T> {
|
2026-01-28 23:06:06 +01:00
|
|
|
let key = resource
|
|
|
|
|
.resource_key()
|
|
|
|
|
.map_err(|e| ApiError::BadRequest(e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
info!("Creating resource: {}", key);
|
|
|
|
|
|
|
|
|
|
// Check if resource already exists
|
|
|
|
|
let storage_key = KeyEncoder::encode_resource_key(&key);
|
|
|
|
|
if state.storage.as_ref().exists(storage_key.as_bytes())? {
|
|
|
|
|
return Err(ApiError::AlreadyExists(format!(
|
|
|
|
|
"Resource already exists: {}",
|
|
|
|
|
key
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set UID and initial resource version
|
|
|
|
|
resource.set_uid(Uuid::new_v4().to_string());
|
|
|
|
|
|
|
|
|
|
// Serialize resource
|
|
|
|
|
let data = serde_json::to_vec(&resource)?;
|
|
|
|
|
|
|
|
|
|
// Create commit
|
2026-01-28 23:17:19 +01:00
|
|
|
let change = Change::create(
|
|
|
|
|
storage_key.clone(),
|
|
|
|
|
String::from_utf8_lossy(&data).to_string(),
|
|
|
|
|
);
|
2026-01-28 23:06:06 +01:00
|
|
|
|
|
|
|
|
let commit = state
|
|
|
|
|
.version_store
|
|
|
|
|
.create_commit(
|
|
|
|
|
CommitBuilder::new()
|
|
|
|
|
.change(change)
|
|
|
|
|
.message(format!("Create {}", key)),
|
|
|
|
|
)
|
|
|
|
|
.map_err(ApiError::from)?;
|
|
|
|
|
|
|
|
|
|
// Set resource version to commit ID
|
|
|
|
|
resource.set_resource_version(reddwarf_core::ResourceVersion::new(commit.id().to_string()));
|
|
|
|
|
|
|
|
|
|
// Store in storage
|
|
|
|
|
state.storage.as_ref().put(storage_key.as_bytes(), &data)?;
|
|
|
|
|
|
|
|
|
|
info!("Created resource: {} with version {}", key, commit.id());
|
2026-02-08 21:29:17 +01:00
|
|
|
|
|
|
|
|
// Publish ADDED event (best-effort)
|
|
|
|
|
if let Ok(object) = serde_json::to_value(&resource) {
|
|
|
|
|
let event = ResourceEvent::added(key, object, commit.id().to_string());
|
|
|
|
|
let _ = state.event_tx.send(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:06:06 +01:00
|
|
|
Ok(resource)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update a resource in storage
|
2026-01-28 23:17:19 +01:00
|
|
|
pub async fn update_resource<T: Resource>(state: &AppState, mut resource: T) -> Result<T> {
|
2026-01-28 23:06:06 +01:00
|
|
|
let key = resource
|
|
|
|
|
.resource_key()
|
|
|
|
|
.map_err(|e| ApiError::BadRequest(e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
info!("Updating resource: {}", key);
|
|
|
|
|
|
|
|
|
|
let storage_key = KeyEncoder::encode_resource_key(&key);
|
|
|
|
|
|
|
|
|
|
// Get previous version
|
|
|
|
|
let prev_data = state
|
|
|
|
|
.storage
|
|
|
|
|
.as_ref()
|
|
|
|
|
.get(storage_key.as_bytes())?
|
|
|
|
|
.ok_or_else(|| ApiError::NotFound(format!("Resource not found: {}", key)))?;
|
|
|
|
|
|
|
|
|
|
// Serialize new resource
|
|
|
|
|
let new_data = serde_json::to_vec(&resource)?;
|
|
|
|
|
|
|
|
|
|
// Create commit
|
|
|
|
|
let change = Change::update(
|
|
|
|
|
storage_key.clone(),
|
|
|
|
|
String::from_utf8_lossy(&new_data).to_string(),
|
|
|
|
|
String::from_utf8_lossy(&prev_data).to_string(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let commit = state
|
|
|
|
|
.version_store
|
|
|
|
|
.create_commit(
|
|
|
|
|
CommitBuilder::new()
|
|
|
|
|
.change(change)
|
|
|
|
|
.message(format!("Update {}", key)),
|
|
|
|
|
)
|
|
|
|
|
.map_err(ApiError::from)?;
|
|
|
|
|
|
|
|
|
|
// Set resource version to commit ID
|
|
|
|
|
resource.set_resource_version(reddwarf_core::ResourceVersion::new(commit.id().to_string()));
|
|
|
|
|
|
|
|
|
|
// Update in storage
|
2026-01-28 23:17:19 +01:00
|
|
|
state
|
|
|
|
|
.storage
|
|
|
|
|
.as_ref()
|
|
|
|
|
.put(storage_key.as_bytes(), &new_data)?;
|
2026-01-28 23:06:06 +01:00
|
|
|
|
|
|
|
|
info!("Updated resource: {} with version {}", key, commit.id());
|
2026-02-08 21:29:17 +01:00
|
|
|
|
|
|
|
|
// Publish MODIFIED event (best-effort)
|
|
|
|
|
if let Ok(object) = serde_json::to_value(&resource) {
|
|
|
|
|
let event = ResourceEvent::modified(key, object, commit.id().to_string());
|
|
|
|
|
let _ = state.event_tx.send(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:06:06 +01:00
|
|
|
Ok(resource)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Delete a resource from storage
|
2026-01-28 23:17:19 +01:00
|
|
|
pub async fn delete_resource(state: &AppState, key: &ResourceKey) -> Result<()> {
|
2026-01-28 23:06:06 +01:00
|
|
|
info!("Deleting resource: {}", key);
|
|
|
|
|
|
|
|
|
|
let storage_key = KeyEncoder::encode_resource_key(key);
|
|
|
|
|
|
|
|
|
|
// Get current version
|
|
|
|
|
let prev_data = state
|
|
|
|
|
.storage
|
|
|
|
|
.as_ref()
|
|
|
|
|
.get(storage_key.as_bytes())?
|
|
|
|
|
.ok_or_else(|| ApiError::NotFound(format!("Resource not found: {}", key)))?;
|
|
|
|
|
|
|
|
|
|
// Create commit
|
2026-01-28 23:17:19 +01:00
|
|
|
let change = Change::delete(
|
|
|
|
|
storage_key.clone(),
|
|
|
|
|
String::from_utf8_lossy(&prev_data).to_string(),
|
|
|
|
|
);
|
2026-01-28 23:06:06 +01:00
|
|
|
|
|
|
|
|
let commit = state
|
|
|
|
|
.version_store
|
|
|
|
|
.create_commit(
|
|
|
|
|
CommitBuilder::new()
|
|
|
|
|
.change(change)
|
|
|
|
|
.message(format!("Delete {}", key)),
|
|
|
|
|
)
|
|
|
|
|
.map_err(ApiError::from)?;
|
|
|
|
|
|
|
|
|
|
// Delete from storage
|
|
|
|
|
state.storage.as_ref().delete(storage_key.as_bytes())?;
|
|
|
|
|
|
|
|
|
|
info!("Deleted resource: {} at version {}", key, commit.id());
|
2026-02-08 21:29:17 +01:00
|
|
|
|
|
|
|
|
// Publish DELETED event with last-known state (best-effort)
|
|
|
|
|
if let Ok(object) = serde_json::from_slice::<serde_json::Value>(&prev_data) {
|
|
|
|
|
let event = ResourceEvent::deleted(key.clone(), object, commit.id().to_string());
|
|
|
|
|
let _ = state.event_tx.send(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:06:06 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List resources with optional filtering
|
2026-01-28 23:17:19 +01:00
|
|
|
pub async fn list_resources<T: Resource>(state: &AppState, prefix: &str) -> Result<Vec<T>> {
|
2026-01-28 23:06:06 +01:00
|
|
|
debug!("Listing resources with prefix: {}", prefix);
|
|
|
|
|
|
|
|
|
|
let results = state.storage.as_ref().scan(prefix.as_bytes())?;
|
|
|
|
|
|
|
|
|
|
let mut resources = Vec::new();
|
|
|
|
|
for (_key, data) in results.iter() {
|
|
|
|
|
let resource: T = serde_json::from_slice(data)?;
|
|
|
|
|
resources.push(resource);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug!("Found {} resources", resources.len());
|
|
|
|
|
Ok(resources)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List response wrapper
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct ListResponse<T: Serialize> {
|
|
|
|
|
#[serde(rename = "apiVersion")]
|
|
|
|
|
pub api_version: String,
|
|
|
|
|
pub kind: String,
|
|
|
|
|
pub items: Vec<T>,
|
|
|
|
|
pub metadata: ListMetadata,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List metadata
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct ListMetadata {
|
|
|
|
|
#[serde(rename = "resourceVersion")]
|
|
|
|
|
pub resource_version: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: Serialize> ListResponse<T> {
|
|
|
|
|
pub fn new(api_version: String, kind: String, items: Vec<T>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
api_version,
|
|
|
|
|
kind,
|
|
|
|
|
items,
|
|
|
|
|
metadata: ListMetadata {
|
|
|
|
|
resource_version: Uuid::new_v4().to_string(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|