use super::system::AppState;
use crate::error::DbError;
use crate::sync::{
protocol::Operation,
session::{ChangeOperation, SyncChange, SyncSession},
LogEntry, VersionVector,
};
use axum::{
extract::{Query, State},
response::Json,
};
use serde::Deserialize;
use serde_json::Value;
fn evaluate_simple_filter(filter_query: &str, doc: &Value) -> bool {
let filter = filter_query.trim();
if filter.is_empty() {
return true;
}
let ops = ["==", "!=", ">=", "<=", ">", "<"];
for op in ops {
if let Some(pos) = filter.find(op) {
let left = filter[..pos].trim();
let right = filter[pos + op.len()..].trim();
let field_value = get_nested_field(doc, left);
let compare_value = parse_filter_value(right);
return match op {
"==" => values_equal(&field_value, &compare_value),
"!=" => !values_equal(&field_value, &compare_value),
">" => compare_numbers(&field_value, &compare_value) > 0,
"<" => compare_numbers(&field_value, &compare_value) < 0,
">=" => compare_numbers(&field_value, &compare_value) >= 0,
"<=" => compare_numbers(&field_value, &compare_value) <= 0,
_ => true,
};
}
}
true
}
fn get_nested_field(doc: &Value, path: &str) -> Value {
let parts: Vec<&str> = path.split('.').collect();
let start = if parts.first() == Some(&"doc") { 1 } else { 0 };
let mut current = doc;
for part in parts.iter().skip(start) {
match current.get(*part) {
Some(v) => current = v,
None => return Value::Null,
}
}
current.clone()
}
fn parse_filter_value(value: &str) -> Value {
let v = value.trim();
if (v.starts_with('\'') && v.ends_with('\'')) || (v.starts_with('"') && v.ends_with('"')) {
return Value::String(v[1..v.len() - 1].to_string());
}
if v == "true" {
return Value::Bool(true);
}
if v == "false" {
return Value::Bool(false);
}
if v == "null" {
return Value::Null;
}
if let Ok(n) = v.parse::<i64>() {
return Value::Number(n.into());
}
if let Ok(n) = v.parse::<f64>() {
return serde_json::Number::from_f64(n)
.map(Value::Number)
.unwrap_or(Value::Null);
}
Value::String(v.to_string())
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::String(s1), Value::String(s2)) => s1 == s2,
(Value::Number(n1), Value::Number(n2)) => {
n1.as_f64().unwrap_or(0.0) == n2.as_f64().unwrap_or(0.0)
}
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Null, Value::Null) => true,
_ => a == b,
}
}
fn compare_numbers(a: &Value, b: &Value) -> i32 {
let a_num = match a {
Value::Number(n) => n.as_f64().unwrap_or(0.0),
_ => 0.0,
};
let b_num = match b {
Value::Number(n) => n.as_f64().unwrap_or(0.0),
_ => 0.0,
};
if a_num < b_num {
-1
} else if a_num > b_num {
1
} else {
0
}
}
fn log_entry_to_sync_change(entry: &LogEntry) -> SyncChange {
SyncChange {
database: entry.database.clone(),
collection: entry.collection.clone(),
document_key: entry.key.clone(),
operation: match entry.operation {
Operation::Insert => ChangeOperation::Insert,
Operation::Update => ChangeOperation::Update,
Operation::Delete => ChangeOperation::Delete,
Operation::CreateCollection
| Operation::DeleteCollection
| Operation::TruncateCollection
| Operation::CreateDatabase
| Operation::DeleteDatabase
| Operation::ColumnarInsert
| Operation::ColumnarCreateCollection => ChangeOperation::Insert,
Operation::ColumnarDelete
| Operation::ColumnarDropCollection
| Operation::ColumnarTruncate => ChangeOperation::Delete,
_ => ChangeOperation::Update,
},
document_data: entry
.data
.as_ref()
.and_then(|d| serde_json::from_slice(d).ok()),
vector: VersionVector::with_node(&entry.node_id, entry.sequence),
timestamp: entry.timestamp,
is_delta: false,
delta_patch: None,
parent_vectors: vec![],
}
}
#[derive(Debug, Deserialize)]
pub struct RegisterSessionRequest {
pub device_id: String,
pub api_key: String,
pub subscriptions: Option<Vec<String>>,
pub filter_query: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SyncPullRequest {
pub session_id: String,
pub client_vector: VersionVector,
pub limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
pub struct SyncPushRequest {
pub session_id: String,
pub changes: Vec<SyncChange>,
pub client_vector: VersionVector,
}
#[derive(Debug, Deserialize)]
pub struct SyncAckRequest {
pub session_id: String,
pub applied_vector: VersionVector,
}
#[derive(Debug, Deserialize)]
pub struct ConflictsQuery {
pub session_id: String,
}
#[derive(Debug, Deserialize)]
pub struct ResolveConflictRequest {
pub session_id: String,
pub document_key: String,
pub resolution: String, pub merged_data: Option<serde_json::Value>,
}
pub async fn register_sync_session(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, DbError> {
let device_id = req
.get("device_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("device_id is required".to_string()))?
.to_string();
let api_key = req
.get("api_key")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("api_key is required".to_string()))?
.to_string();
let subscriptions: Vec<String> = req
.get("subscriptions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let filter_query = req
.get("filter_query")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let cluster_secret = state.cluster_secret();
let secret_bytes = cluster_secret.as_bytes();
let mut session = if secret_bytes.is_empty() {
let session_id = format!("{}-{}", device_id, uuid::Uuid::new_v4());
SyncSession::new(session_id, device_id.clone(), api_key)
} else {
SyncSession::new_secure(&device_id, &api_key, secret_bytes)
};
let session_id = session.session_id.clone();
session.subscriptions = subscriptions;
session.filter_query = filter_query;
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
session_manager.register_session(session).await;
let server_vector = VersionVector::new();
let capabilities = serde_json::json!({
"delta_sync": true,
"crdt_types": true,
"compression": true,
"max_batch_size": 1048576, });
Ok(Json(serde_json::json!({
"session_id": session_id,
"server_vector": server_vector,
"capabilities": capabilities,
})))
}
pub async fn pull_changes(
State(state): State<AppState>,
axum::Extension(claims): axum::Extension<crate::server::auth::Claims>,
Json(req): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, DbError> {
let session_id = req
.get("session_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("session_id is required".to_string()))?
.to_string();
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
let session = session_manager
.get_session(&session_id)
.await
.ok_or_else(|| DbError::BadRequest(format!("Session '{}' not found", session_id)))?;
let cluster_secret = state.cluster_secret();
if !cluster_secret.is_empty()
&& !SyncSession::verify_session_id(&session_id, &session.api_key, cluster_secret.as_bytes())
{
return Err(DbError::BadRequest("Invalid session signature".to_string()));
}
let _client_vector: VersionVector = req
.get("client_vector")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_else(VersionVector::new);
let limit = req
.get("limit")
.and_then(|v| v.as_u64())
.map(|n| n as usize)
.unwrap_or(100);
let subscriptions = &session.subscriptions;
let after_sequence = session.last_sequence;
let sync_log = state
.replication_log
.as_ref()
.ok_or_else(|| DbError::InternalError("Replication log not initialized".to_string()))?;
let log_entries = sync_log.get_entries_after(after_sequence, limit);
let filtered: Vec<_> = log_entries
.into_iter()
.filter(|e| subscriptions.is_empty() || subscriptions.contains(&e.collection))
.collect();
let filter_query = &session.filter_query;
let filtered: Vec<_> = if let Some(ref filter) = filter_query {
filtered
.into_iter()
.filter(|entry| {
if entry.operation == Operation::Delete {
return true;
}
entry
.data
.as_ref()
.and_then(|d| serde_json::from_slice::<Value>(d).ok())
.map(|doc| evaluate_simple_filter(filter, &doc))
.unwrap_or(true) })
.collect()
} else {
filtered
};
let permissions =
crate::server::AuthorizationService::get_effective_permissions(&claims, &state).await?;
let scoped = claims.scoped_databases.as_deref();
let mut allowed_dbs: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
let filtered: Vec<_> = filtered
.into_iter()
.filter(|e| {
*allowed_dbs.entry(e.database.clone()).or_insert_with(|| {
crate::server::authz_middleware::enforce_raw(
&permissions,
crate::server::PermissionAction::Read,
Some(&e.database),
scoped,
&claims.sub,
)
})
})
.collect();
let changes: Vec<SyncChange> = filtered.iter().map(log_entry_to_sync_change).collect();
let has_more = changes.len() == limit;
let max_seq = filtered
.iter()
.map(|e| e.sequence)
.max()
.unwrap_or(after_sequence);
let mut server_vector = VersionVector::new();
for entry in &filtered {
let current = server_vector.get(&entry.node_id);
if entry.sequence > current {
server_vector.increment(&entry.node_id);
while server_vector.get(&entry.node_id) < entry.sequence {
server_vector.increment(&entry.node_id);
}
}
}
session_manager
.update_session_sequence(&session_id, max_seq)
.await;
session_manager
.update_session_vector(&session_id, &server_vector)
.await;
let conflicts: Vec<serde_json::Value> = vec![];
Ok(Json(serde_json::json!({
"changes": changes,
"server_vector": server_vector,
"has_more": has_more,
"conflicts": conflicts,
})))
}
fn apply_sync_change(state: &AppState, change: &SyncChange) -> Result<(), DbError> {
if change.is_delta {
return Err(DbError::OperationNotSupported(
"delta sync changes are not supported; push the full document".to_string(),
));
}
if matches!(
change.operation,
ChangeOperation::Insert | ChangeOperation::Update
) {
if state.storage.get_database(&change.database).is_err() {
let _ = state.storage.create_database(change.database.clone());
}
if let Ok(db) = state.storage.get_database(&change.database) {
if db.get_collection(&change.collection).is_err() {
let _ = db.create_collection(change.collection.clone(), None);
}
}
}
let db = state.storage.get_database(&change.database)?;
let collection = db.get_collection(&change.collection)?;
match change.operation {
ChangeOperation::Insert | ChangeOperation::Update => {
let data = change.document_data.clone().ok_or_else(|| {
DbError::BadRequest(format!(
"change for '{}' has no document_data",
change.document_key
))
})?;
collection.upsert_batch(vec![(change.document_key.clone(), data)])?;
}
ChangeOperation::Delete => {
if let Err(e) = collection.delete(&change.document_key) {
if !matches!(e, DbError::DocumentNotFound(_)) {
return Err(e);
}
}
}
}
Ok(())
}
pub async fn push_changes(
State(state): State<AppState>,
axum::Extension(claims): axum::Extension<crate::server::auth::Claims>,
Json(req): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, DbError> {
let session_id = req
.get("session_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("session_id is required".to_string()))?
.to_string();
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
let session = session_manager
.get_session(&session_id)
.await
.ok_or_else(|| DbError::BadRequest(format!("Session '{}' not found", session_id)))?;
let cluster_secret = state.cluster_secret();
if !cluster_secret.is_empty()
&& !SyncSession::verify_session_id(&session_id, &session.api_key, cluster_secret.as_bytes())
{
return Err(DbError::BadRequest("Invalid session signature".to_string()));
}
let changes: Vec<SyncChange> = req
.get("changes")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
let client_vector: VersionVector = req
.get("client_vector")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_else(VersionVector::new);
let permissions =
crate::server::AuthorizationService::get_effective_permissions(&claims, &state).await?;
let scoped = claims.scoped_databases.as_deref();
let mut writable_dbs: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
let conflicts: Vec<serde_json::Value> = Vec::new();
let mut accepted = 0;
let mut rejected = 0;
for change in &changes {
let writable = *writable_dbs
.entry(change.database.clone())
.or_insert_with(|| {
crate::server::authz_middleware::enforce_raw(
&permissions,
crate::server::PermissionAction::Write,
Some(&change.database),
scoped,
&claims.sub,
)
});
if !writable {
rejected += 1;
continue;
}
if let Err(e) = apply_sync_change(&state, change) {
tracing::warn!(
"sync push: failed to apply {:?} on {}/{} key {}: {}",
change.operation,
change.database,
change.collection,
change.document_key,
e
);
rejected += 1;
continue;
}
accepted += 1;
if let Some(ref log) = state.replication_log {
let operation = match change.operation {
ChangeOperation::Insert => Operation::Insert,
ChangeOperation::Update => Operation::Update,
ChangeOperation::Delete => Operation::Delete,
};
let data_bytes = change
.document_data
.as_ref()
.and_then(|d| serde_json::to_vec(d).ok());
let entry = LogEntry {
sequence: 0, node_id: session.device_id.clone(),
database: change.database.clone(),
collection: change.collection.clone(),
operation,
key: change.document_key.clone(),
data: data_bytes,
timestamp: change.timestamp,
origin_sequence: None,
};
let _ = log.append(entry);
}
}
let mut server_vector = client_vector.clone();
server_vector.increment(&session.device_id);
session_manager
.update_session_vector(&session_id, &server_vector)
.await;
Ok(Json(serde_json::json!({
"server_vector": server_vector,
"conflicts": conflicts,
"accepted": accepted,
"rejected": rejected,
})))
}
pub async fn acknowledge_changes(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, DbError> {
let session_id = req
.get("session_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("session_id is required".to_string()))?
.to_string();
let applied_vector: VersionVector = req
.get("applied_vector")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_else(VersionVector::new);
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
let _session = session_manager
.get_session(&session_id)
.await
.ok_or_else(|| DbError::BadRequest(format!("Session '{}' not found", session_id)))?;
session_manager
.update_session_vector(&session_id, &applied_vector)
.await;
Ok(Json(serde_json::json!({
"success": true,
})))
}
pub async fn list_conflicts(
State(state): State<AppState>,
Query(params): Query<ConflictsQuery>,
) -> Result<Json<serde_json::Value>, DbError> {
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
let _session = session_manager
.get_session(¶ms.session_id)
.await
.ok_or_else(|| DbError::BadRequest(format!("Session '{}' not found", params.session_id)))?;
Err(DbError::OperationNotSupported(
"conflict listing is not implemented: documents do not carry version \
vectors, so concurrent writes cannot be detected. Pushes currently \
resolve last-write-wins."
.to_string(),
))
}
pub async fn resolve_conflict(
State(state): State<AppState>,
Json(req): Json<serde_json::Value>,
) -> Result<Json<serde_json::Value>, DbError> {
let session_id = req
.get("session_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("session_id is required".to_string()))?
.to_string();
let document_key = req
.get("document_key")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("document_key is required".to_string()))?
.to_string();
let resolution = req
.get("resolution")
.and_then(|v| v.as_str())
.ok_or_else(|| DbError::BadRequest("resolution is required".to_string()))?
.to_string();
let merged_data = req.get("merged_data").cloned();
let session_manager = state.sync_session_manager.as_ref().ok_or_else(|| {
DbError::InternalError("Sync session manager not initialized".to_string())
})?;
let _session = session_manager
.get_session(&session_id)
.await
.ok_or_else(|| DbError::BadRequest(format!("Session '{}' not found", session_id)))?;
if !matches!(resolution.as_str(), "local" | "remote" | "merged") {
return Err(DbError::BadRequest(
"resolution must be 'local', 'remote', or 'merged'".to_string(),
));
}
if resolution == "merged" && merged_data.is_none() {
return Err(DbError::BadRequest(
"merged_data is required when resolution is 'merged'".to_string(),
));
}
Err(DbError::OperationNotSupported(format!(
"conflict resolution is not implemented: no conflict is recorded for \
document '{}'. Pushes currently resolve last-write-wins.",
document_key
)))
}